From 5bbe93bcec5c3ac042d4072bf04e60893adf01cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Thu, 25 Jan 2024 13:10:20 -0600 Subject: [PATCH] feat: provisions --- scripts/buildAssets/circleFlags.ts | 2 +- scripts/buildAssets/constants.ts | 1 + scripts/buildAssets/consumableIcons.ts | 10 +- scripts/buildAssets/definitions.ts | 129 ++++++++++++++---- scripts/buildAssets/index.ts | 12 +- .../Configure/components/Consumables.tsx | 10 +- .../Configure/components/Provisions.tsx | 63 +++++++++ .../[id]/components/Configure/index.tsx | 6 +- src/components/ModuleButton.tsx | 8 +- .../blitzkrieg/checkConsumableInclusivity.ts | 11 +- ...efinitions.ts => consumableDefinitions.ts} | 10 +- src/core/blitzkrieg/provisionDefinitions.ts | 21 +++ src/core/blitzkrieg/tankDefinitions.ts | 1 + src/stores/tankopedia.ts | 2 + 14 files changed, 233 insertions(+), 53 deletions(-) create mode 100644 src/app/tools/tankopedia/[id]/components/Configure/components/Provisions.tsx rename src/core/blitzkrieg/{consumablesDefinitions.ts => consumableDefinitions.ts} (71%) create mode 100644 src/core/blitzkrieg/provisionDefinitions.ts diff --git a/scripts/buildAssets/circleFlags.ts b/scripts/buildAssets/circleFlags.ts index 4b32bdeb6..d293f2bb7 100644 --- a/scripts/buildAssets/circleFlags.ts +++ b/scripts/buildAssets/circleFlags.ts @@ -4,7 +4,7 @@ import { commitAssets } from '../../src/core/blitzkrieg/commitAssets'; import { FileChange } from '../../src/core/blitzkrieg/commitMultipleFiles'; import { DATA, POI } from './constants'; -export async function buildCircleFlags(production: boolean) { +export async function circleFlags(production: boolean) { const flags = await readdir(`${DATA}/${POI.flags}`); const changes = await Promise.all( flags diff --git a/scripts/buildAssets/constants.ts b/scripts/buildAssets/constants.ts index c73d99f0c..4e3132b80 100644 --- a/scripts/buildAssets/constants.ts +++ b/scripts/buildAssets/constants.ts @@ -21,6 +21,7 @@ export const POI = { optionalDevicesImage: 'Gfx/UI/InventoryIcons/Big/OptionalDevices/texture0.packed.webp', consumablesCommon: 'XML/item_defs/vehicles/common/consumables/common.xml', + provisionsCommon: 'XML/item_defs/vehicles/common/provisions/common.xml', consumableIcons: 'Gfx/UI/InventoryIcons/Big/Consumables', equipmentItemImageMappings: 'UI/Screens3/Lobby/Inventory/Equipment/EquipmentItemImage.style.yaml', diff --git a/scripts/buildAssets/consumableIcons.ts b/scripts/buildAssets/consumableIcons.ts index f65719abd..591f134fd 100644 --- a/scripts/buildAssets/consumableIcons.ts +++ b/scripts/buildAssets/consumableIcons.ts @@ -6,7 +6,7 @@ import { readYAMLDVPL } from '../../src/core/blitz/readYAMLDVPL'; import { commitAssets } from '../../src/core/blitzkrieg/commitAssets'; import { FileChange } from '../../src/core/blitzkrieg/commitMultipleFiles'; import { DATA, POI } from './constants'; -import { ConsumablesCommon } from './definitions'; +import { ConsumablesProvisionsCommon } from './definitions'; interface Mappings { Header: { version: number }; @@ -16,7 +16,7 @@ interface Mappings { }[]; } -export async function buildConsumableIcons(production: boolean) { +export async function consumableIcons(production: boolean) { console.log('Building consumable icons...'); const changes: FileChange[] = []; @@ -29,9 +29,9 @@ export async function buildConsumableIcons(production: boolean) { const equipmentItemImageMappings = await readYAMLDVPL( `${DATA}/${POI.equipmentItemImageMappings}.dvpl`, ); - const consumablesCommon = await readXMLDVPL<{ root: ConsumablesCommon }>( - `${DATA}/${POI.consumablesCommon}.dvpl`, - ); + const consumablesCommon = await readXMLDVPL<{ + root: ConsumablesProvisionsCommon; + }>(`${DATA}/${POI.consumablesCommon}.dvpl`); const styleSheets = Object.values(equipmentItemImageMappings.StyleSheets); await Promise.all( diff --git a/scripts/buildAssets/definitions.ts b/scripts/buildAssets/definitions.ts index f6ea2e4f4..01fff3bd0 100644 --- a/scripts/buildAssets/definitions.ts +++ b/scripts/buildAssets/definitions.ts @@ -8,9 +8,9 @@ import { readYAMLDVPL } from '../../src/core/blitz/readYAMLDVPL'; import { toUniqueId } from '../../src/core/blitz/toUniqueId'; import { commitAssets } from '../../src/core/blitzkrieg/commitAssets'; import { - ConsumableDefinitionFilterCategory, ConsumableDefinitions, -} from '../../src/core/blitzkrieg/consumablesDefinitions'; + TankFilterDefinitionCategory, +} from '../../src/core/blitzkrieg/consumableDefinitions'; import { EquipmentDefinitions, EquipmentPreset, @@ -20,6 +20,7 @@ import { ModelArmor, ModelDefinitions, } from '../../src/core/blitzkrieg/modelDefinitions'; +import { ProvisionDefinitions } from '../../src/core/blitzkrieg/provisionDefinitions'; import { GunDefinition, ShellType, @@ -61,6 +62,7 @@ type VehicleDefinitionArmor = Record< interface VehicleDefinitions { invisibility: { moving: number; still: number; firePenalty: number }; consumableSlots: number; + provisionSlots: number; optDevicePreset: string; hull: { armor: VehicleDefinitionArmor; @@ -190,7 +192,7 @@ interface OptionalDeviceSlotRow { [key: string]: { device0: string; device1: string }; } -export interface ConsumablesCommon { +export interface ConsumablesProvisionsCommon { [key: string]: { id: number; userString: string; @@ -198,7 +200,7 @@ export interface ConsumablesCommon { icon: string; category: string; tags: string; - vehicleFilter: { + vehicleFilter?: { include: { vehicle: ConsumablesFilter }; exclude?: { vehicle: ConsumablesFilter }; }; @@ -218,7 +220,7 @@ type ConsumablesFilter = | { name: string } | { extendedTags: string }; -const consumableEffectSuffixes = [ +const consumableProvisionEffectSuffixes = [ 'Increase', 'Factor', 'Bias', @@ -241,7 +243,7 @@ const missingStrings: Record = { '#artefacts:tungstentip/name': 'Tungsten Shells', }; -export async function buildDefinitions(production: boolean) { +export async function definitions(production: boolean) { console.log('Building tank definitions...'); const tankDefinitions: TankDefinitions = {}; @@ -251,6 +253,7 @@ export async function buildDefinitions(production: boolean) { equipments: {}, }; const consumableDefinitions: ConsumableDefinitions = {}; + const provisionDefinitions: ProvisionDefinitions = {}; const nations = await readdir(`${DATA}/${POI.vehicleDefinitions}`).then( (nations) => nations.filter((nation) => nation !== 'common'), ); @@ -267,9 +270,12 @@ export async function buildDefinitions(production: boolean) { const optionalDeviceSlots = await readXMLDVPL<{ root: OptionalDeviceSlots; }>(`${DATA}/${POI.optionalDeviceSlots}.dvpl`); - const consumables = await readXMLDVPL<{ root: ConsumablesCommon }>( + const consumables = await readXMLDVPL<{ root: ConsumablesProvisionsCommon }>( `${DATA}/${POI.consumablesCommon}.dvpl`, ); + const provisions = await readXMLDVPL<{ root: ConsumablesProvisionsCommon }>( + `${DATA}/${POI.provisionsCommon}.dvpl`, + ); await Promise.all( nations.map(async (nation) => { @@ -342,6 +348,7 @@ export async function buildDefinitions(production: boolean) { id: tankId, equipment, consumables: tankDefinition.root.consumableSlots, + provisions: tankDefinition.root.provisionSlots, name: (tank.shortUserString ? strings[tank.shortUserString] ?? @@ -646,21 +653,23 @@ export async function buildDefinitions(production: boolean) { include: [], }; - const includeRaw = consumable.vehicleFilter.include.vehicle; - const excludeRaw = consumable.vehicleFilter.exclude?.vehicle; + const includeRaw = consumable.vehicleFilter?.include.vehicle; + const excludeRaw = consumable.vehicleFilter?.exclude?.vehicle; - if ('minLevel' in includeRaw) { - consumableDefinitions[consumable.id].include.push({ - type: 'tier', - min: includeRaw.minLevel, - max: includeRaw.maxLevel, - }); - } else if ('name' in includeRaw) { - consumableDefinitions[consumable.id].include.push({ - type: 'ids', - ids: includeRaw.name.split(' ').map((key) => tankStringIdMap[key]), - }); - } else throw new SyntaxError('Unhandled include type'); + if (includeRaw) { + if ('minLevel' in includeRaw) { + consumableDefinitions[consumable.id].include.push({ + type: 'tier', + min: includeRaw.minLevel, + max: includeRaw.maxLevel, + }); + } else if ('name' in includeRaw) { + consumableDefinitions[consumable.id].include.push({ + type: 'ids', + ids: includeRaw.name.split(' ').map((key) => tankStringIdMap[key]), + }); + } else throw new SyntaxError('Unhandled include type'); + } if (excludeRaw) { consumableDefinitions[consumable.id].exclude = []; @@ -675,7 +684,7 @@ export async function buildDefinitions(production: boolean) { type: 'category', categories: excludeRaw.extendedTags.split( ' ', - ) as ConsumableDefinitionFilterCategory[], + ) as TankFilterDefinitionCategory[], }); } else throw new SyntaxError('Unhandled exclude type'); } @@ -685,7 +694,9 @@ export async function buildDefinitions(production: boolean) { ...consumable.script.bonusValues, }).forEach(([effectName, effect]) => { if ( - !consumableEffectSuffixes.some((suffix) => effectName.endsWith(suffix)) + !consumableProvisionEffectSuffixes.some((suffix) => + effectName.endsWith(suffix), + ) ) { return; } @@ -699,6 +710,73 @@ export async function buildDefinitions(production: boolean) { }); }); + Object.values(provisions.root).forEach((provision) => { + provisionDefinitions[provision.id] = { + id: provision.id, + name: + strings[provision.userString] ?? + missingStrings[provision.userString] ?? + `Unknown ${provision.id}`, + include: [], + }; + + const includeRaw = provision.vehicleFilter?.include.vehicle; + const excludeRaw = provision.vehicleFilter?.exclude?.vehicle; + + if (includeRaw) { + if ('minLevel' in includeRaw) { + provisionDefinitions[provision.id].include.push({ + type: 'tier', + min: includeRaw.minLevel, + max: includeRaw.maxLevel, + }); + } else if ('name' in includeRaw) { + provisionDefinitions[provision.id].include.push({ + type: 'ids', + ids: includeRaw.name.split(' ').map((key) => tankStringIdMap[key]), + }); + } else throw new SyntaxError('Unhandled include type'); + } + + if (excludeRaw) { + provisionDefinitions[provision.id].exclude = []; + + if ('name' in excludeRaw) { + provisionDefinitions[provision.id].exclude!.push({ + type: 'ids', + ids: excludeRaw.name.split(' ').map((key) => tankStringIdMap[key]), + }); + } else if ('extendedTags' in excludeRaw) { + provisionDefinitions[provision.id].exclude!.push({ + type: 'category', + categories: excludeRaw.extendedTags.split( + ' ', + ) as TankFilterDefinitionCategory[], + }); + } else throw new SyntaxError('Unhandled exclude type'); + } + + Object.entries({ + ...provision.script, + ...provision.script.bonusValues, + }).forEach(([effectName, effect]) => { + if ( + !consumableProvisionEffectSuffixes.some((suffix) => + effectName.endsWith(suffix), + ) + ) { + return; + } + + if (!provisionDefinitions[provision.id].effects) { + provisionDefinitions[provision.id].effects = {}; + } + + provisionDefinitions[provision.id].effects![effectName] = + effect as number; + }); + }); + await commitAssets( 'definitions', [ @@ -722,6 +800,11 @@ export async function buildDefinitions(production: boolean) { encoding: 'utf-8', path: 'definitions/consumables.json', }, + { + content: JSON.stringify(provisionDefinitions), + encoding: 'utf-8', + path: 'definitions/provisions.json', + }, ], production, ); diff --git a/scripts/buildAssets/index.ts b/scripts/buildAssets/index.ts index 6aafe8ec4..3aae2e49d 100644 --- a/scripts/buildAssets/index.ts +++ b/scripts/buildAssets/index.ts @@ -1,9 +1,9 @@ import { config } from 'dotenv'; import { argv } from 'process'; import { camouflageIcons } from './camouflageIcons'; -import { buildCircleFlags } from './circleFlags'; -import { buildConsumableIcons } from './consumableIcons'; -import { buildDefinitions } from './definitions'; +import { circleFlags } from './circleFlags'; +import { consumableIcons } from './consumableIcons'; +import { definitions } from './definitions'; import { equipmentIcons } from './equipmentIcons'; import { buildModuleIcons } from './moduleIcons'; import { buildScratchedFlags } from './scratchedFlags'; @@ -26,14 +26,14 @@ if (!targets && !allTargets) throw new Error('No target(s) specified'); const methods = [ buildTankModels, buildTankArmors, - buildDefinitions, + definitions, buildTankIcons, buildScratchedFlags, - buildCircleFlags, + circleFlags, buildShellIcons, buildModuleIcons, equipmentIcons, - buildConsumableIcons, + consumableIcons, camouflageIcons, ]; diff --git a/src/app/tools/tankopedia/[id]/components/Configure/components/Consumables.tsx b/src/app/tools/tankopedia/[id]/components/Configure/components/Consumables.tsx index f2d24596c..75267ad49 100644 --- a/src/app/tools/tankopedia/[id]/components/Configure/components/Consumables.tsx +++ b/src/app/tools/tankopedia/[id]/components/Configure/components/Consumables.tsx @@ -1,8 +1,8 @@ import { Flex, Heading, Text } from '@radix-ui/themes'; import { use } from 'react'; import { ModuleButton } from '../../../../../../../components/ModuleButton'; -import { checkConsumableInclusivity } from '../../../../../../../core/blitzkrieg/checkConsumableInclusivity'; -import { consumableDefinitions } from '../../../../../../../core/blitzkrieg/consumablesDefinitions'; +import { checkConsumableProvisionInclusivity } from '../../../../../../../core/blitzkrieg/checkConsumableInclusivity'; +import { consumableDefinitions } from '../../../../../../../core/blitzkrieg/consumableDefinitions'; import { useDuel } from '../../../../../../../stores/duel'; import { mutateTankopediaTemporary, @@ -15,7 +15,11 @@ export function Consumables() { const consumables = useTankopediaTemporary((state) => state.consumables); const consumablesList = Object.values(awaitedConsumableDefinitions).filter( (consumable) => - checkConsumableInclusivity(consumable, protagonist.tank, protagonist.gun), + checkConsumableProvisionInclusivity( + consumable, + protagonist.tank, + protagonist.gun, + ), ); return ( diff --git a/src/app/tools/tankopedia/[id]/components/Configure/components/Provisions.tsx b/src/app/tools/tankopedia/[id]/components/Configure/components/Provisions.tsx new file mode 100644 index 000000000..0c9fb0550 --- /dev/null +++ b/src/app/tools/tankopedia/[id]/components/Configure/components/Provisions.tsx @@ -0,0 +1,63 @@ +import { Flex, Heading, Text } from '@radix-ui/themes'; +import { use } from 'react'; +import { ModuleButton } from '../../../../../../../components/ModuleButton'; +import { checkConsumableProvisionInclusivity } from '../../../../../../../core/blitzkrieg/checkConsumableInclusivity'; +import { provisionDefinitions } from '../../../../../../../core/blitzkrieg/provisionDefinitions'; +import { useDuel } from '../../../../../../../stores/duel'; +import { + mutateTankopediaTemporary, + useTankopediaTemporary, +} from '../../../../../../../stores/tankopedia'; + +export function Provisions() { + const protagonist = useDuel((state) => state.protagonist!); + const awaitedProvisionDefinitions = use(provisionDefinitions); + const provisions = useTankopediaTemporary((state) => state.provisions); + const provisionsList = Object.values(awaitedProvisionDefinitions).filter( + (provision) => + checkConsumableProvisionInclusivity( + provision, + protagonist.tank, + protagonist.gun, + ), + ); + + return ( + + + Provisions (max {protagonist.tank.provisions}) + + + + {provisionsList.map((provision, index) => { + const selected = provisions.includes(provision.id); + + return ( + { + mutateTankopediaTemporary((draft) => { + if (selected) { + draft.provisions = draft.provisions.filter( + (id) => id !== provision.id, + ); + } else { + draft.provisions.push(provision.id); + } + }); + }} + /> + ); + })} + + + ); +} diff --git a/src/app/tools/tankopedia/[id]/components/Configure/index.tsx b/src/app/tools/tankopedia/[id]/components/Configure/index.tsx index b00a07843..4c8181424 100644 --- a/src/app/tools/tankopedia/[id]/components/Configure/index.tsx +++ b/src/app/tools/tankopedia/[id]/components/Configure/index.tsx @@ -3,6 +3,7 @@ import { Camouflage } from './components/Camouflage'; import { Consumables } from './components/Consumables'; import { Equipments } from './components/Equipments'; import { Modules } from './components/Modules'; +import { Provisions } from './components/Provisions'; export function Configure() { return ( @@ -14,13 +15,10 @@ export function Configure() { + {/* - - Camouflage - - Provisions */} diff --git a/src/components/ModuleButton.tsx b/src/components/ModuleButton.tsx index 8984061d8..97e25c09e 100644 --- a/src/components/ModuleButton.tsx +++ b/src/components/ModuleButton.tsx @@ -25,6 +25,10 @@ type ModuleButtonProps = Omit, 'type'> & { type: 'consumable'; consumable: number; } + | { + type: 'provision'; + provision: number; + } | { type: 'camouflage'; } @@ -89,7 +93,9 @@ export function ModuleButton({ ? `icons/consumables/${props.consumable}.webp` : props.type === 'camouflage' ? `icons/camo.webp` - : `icons/equipment/${props.equipment}.webp`, + : props.type === 'provision' + ? `icons/provisions/${props.provision}.webp` + : `icons/equipment/${props.equipment}.webp`, )} style={imageStyles} /> diff --git a/src/core/blitzkrieg/checkConsumableInclusivity.ts b/src/core/blitzkrieg/checkConsumableInclusivity.ts index 02f8175e2..05f16a1da 100644 --- a/src/core/blitzkrieg/checkConsumableInclusivity.ts +++ b/src/core/blitzkrieg/checkConsumableInclusivity.ts @@ -1,12 +1,13 @@ -import { ConsumableEntry } from './consumablesDefinitions'; +import { ConsumableEntry } from './consumableDefinitions'; +import { ProvisionEntry } from './provisionDefinitions'; import { GunDefinition, TankDefinition } from './tankDefinitions'; -export function checkConsumableInclusivity( - consumable: ConsumableEntry, +export function checkConsumableProvisionInclusivity( + consumableProvision: ConsumableEntry | ProvisionEntry, tank: TankDefinition, gun: GunDefinition, ) { - const included = consumable.include.every((rule) => { + const included = consumableProvision.include.every((rule) => { switch (rule.type) { case 'tier': return rule.min <= tank.tier && tank.tier <= rule.max; @@ -18,7 +19,7 @@ export function checkConsumableInclusivity( throw new SyntaxError('Category filtering found in include rule'); } }); - const excluded = consumable.exclude?.some((rule) => { + const excluded = consumableProvision.exclude?.some((rule) => { switch (rule.type) { case 'tier': return rule.min <= tank.tier && tank.tier <= rule.max; diff --git a/src/core/blitzkrieg/consumablesDefinitions.ts b/src/core/blitzkrieg/consumableDefinitions.ts similarity index 71% rename from src/core/blitzkrieg/consumablesDefinitions.ts rename to src/core/blitzkrieg/consumableDefinitions.ts index fb5d8b7d4..270d67429 100644 --- a/src/core/blitzkrieg/consumablesDefinitions.ts +++ b/src/core/blitzkrieg/consumableDefinitions.ts @@ -1,8 +1,8 @@ import { asset } from './asset'; -export type ConsumableDefinitionFilterCategory = 'clip'; +export type TankFilterDefinitionCategory = 'clip'; -type ConsumableDefinitionFilter = +export type TankFilterDefinition = | { type: 'tier'; min: number; @@ -14,15 +14,15 @@ type ConsumableDefinitionFilter = } | { type: 'category'; - categories: ConsumableDefinitionFilterCategory[]; + categories: TankFilterDefinitionCategory[]; }; export interface ConsumableEntry { id: number; name: string; effects?: Record; - include: ConsumableDefinitionFilter[]; - exclude?: ConsumableDefinitionFilter[]; + include: TankFilterDefinition[]; + exclude?: TankFilterDefinition[]; } export interface ConsumableDefinitions { diff --git a/src/core/blitzkrieg/provisionDefinitions.ts b/src/core/blitzkrieg/provisionDefinitions.ts new file mode 100644 index 000000000..e078bb36e --- /dev/null +++ b/src/core/blitzkrieg/provisionDefinitions.ts @@ -0,0 +1,21 @@ +import { asset } from './asset'; +import { TankFilterDefinition } from './consumableDefinitions'; + +export interface ProvisionEntry { + id: number; + name: string; + effects?: Record; + include: TankFilterDefinition[]; + exclude?: TankFilterDefinition[]; +} + +export interface ProvisionDefinitions { + [key: string]: ProvisionEntry; +} + +export const provisionDefinitions = fetch( + asset('definitions/provisions.json'), + { + cache: 'no-cache', + }, +).then(async (response) => response.json() as Promise); diff --git a/src/core/blitzkrieg/tankDefinitions.ts b/src/core/blitzkrieg/tankDefinitions.ts index 4d33bb0b8..0a10dee47 100644 --- a/src/core/blitzkrieg/tankDefinitions.ts +++ b/src/core/blitzkrieg/tankDefinitions.ts @@ -20,6 +20,7 @@ export interface TankDefinition { name_full?: string; tree_type: TreeTypeString; consumables: number; + provisions: number; tier: Tier; type: TankType; testing?: boolean; diff --git a/src/stores/tankopedia.ts b/src/stores/tankopedia.ts index fa31e2bae..1273ecfa6 100644 --- a/src/stores/tankopedia.ts +++ b/src/stores/tankopedia.ts @@ -58,6 +58,7 @@ interface TankopediaTemporary { equipmentMatrix: EquipmentMatrix; mode: TankopediaMode; consumables: number[]; + provisions: number[]; camouflage: boolean; } @@ -107,6 +108,7 @@ export const useTankopediaTemporary = create()( ], mode: 'model', consumables: [], + provisions: [], camouflage: false, })), );