feat: provisions

This commit is contained in:
TrèsAbhi committed 2024-01-25 13:10:20 -06:00
1 parent 561b59d235
commit 5bbe93bcec
14 files changed
+220 -40

No files matched your search

+1 -1
View File
@@ -4,7 +4,7 @@ import { commitAssets } from '../../src/core/blitzkrieg/commitAssets';
import { FileChange } from '../../src/core/blitzkrieg/commitMultipleFiles'; import { FileChange } from '../../src/core/blitzkrieg/commitMultipleFiles';
import { DATA, POI } from './constants'; 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 flags = await readdir(`${DATA}/${POI.flags}`);
const changes = await Promise.all( const changes = await Promise.all(
flags flags
+1
View File
@@ -21,6 +21,7 @@ export const POI = {
optionalDevicesImage: optionalDevicesImage:
'Gfx/UI/InventoryIcons/Big/OptionalDevices/texture0.packed.webp', 'Gfx/UI/InventoryIcons/Big/OptionalDevices/texture0.packed.webp',
consumablesCommon: 'XML/item_defs/vehicles/common/consumables/common.xml', consumablesCommon: 'XML/item_defs/vehicles/common/consumables/common.xml',
provisionsCommon: 'XML/item_defs/vehicles/common/provisions/common.xml',
consumableIcons: 'Gfx/UI/InventoryIcons/Big/Consumables', consumableIcons: 'Gfx/UI/InventoryIcons/Big/Consumables',
equipmentItemImageMappings: equipmentItemImageMappings:
'UI/Screens3/Lobby/Inventory/Equipment/EquipmentItemImage.style.yaml', 'UI/Screens3/Lobby/Inventory/Equipment/EquipmentItemImage.style.yaml',
+5 -5
View File
@@ -6,7 +6,7 @@ import { readYAMLDVPL } from '../../src/core/blitz/readYAMLDVPL';
import { commitAssets } from '../../src/core/blitzkrieg/commitAssets'; import { commitAssets } from '../../src/core/blitzkrieg/commitAssets';
import { FileChange } from '../../src/core/blitzkrieg/commitMultipleFiles'; import { FileChange } from '../../src/core/blitzkrieg/commitMultipleFiles';
import { DATA, POI } from './constants'; import { DATA, POI } from './constants';
import { ConsumablesCommon } from './definitions'; import { ConsumablesProvisionsCommon } from './definitions';
interface Mappings { interface Mappings {
Header: { version: number }; 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...'); console.log('Building consumable icons...');
const changes: FileChange[] = []; const changes: FileChange[] = [];
@@ -29,9 +29,9 @@ export async function buildConsumableIcons(production: boolean) {
const equipmentItemImageMappings = await readYAMLDVPL<Mappings>( const equipmentItemImageMappings = await readYAMLDVPL<Mappings>(
`${DATA}/${POI.equipmentItemImageMappings}.dvpl`, `${DATA}/${POI.equipmentItemImageMappings}.dvpl`,
); );
const consumablesCommon = await readXMLDVPL<{ root: ConsumablesCommon }>( const consumablesCommon = await readXMLDVPL<{
`${DATA}/${POI.consumablesCommon}.dvpl`, root: ConsumablesProvisionsCommon;
); }>(`${DATA}/${POI.consumablesCommon}.dvpl`);
const styleSheets = Object.values(equipmentItemImageMappings.StyleSheets); const styleSheets = Object.values(equipmentItemImageMappings.StyleSheets);
await Promise.all( await Promise.all(
+94 -11
View File
@@ -8,9 +8,9 @@ import { readYAMLDVPL } from '../../src/core/blitz/readYAMLDVPL';
import { toUniqueId } from '../../src/core/blitz/toUniqueId'; import { toUniqueId } from '../../src/core/blitz/toUniqueId';
import { commitAssets } from '../../src/core/blitzkrieg/commitAssets'; import { commitAssets } from '../../src/core/blitzkrieg/commitAssets';
import { import {
ConsumableDefinitionFilterCategory,
ConsumableDefinitions, ConsumableDefinitions,
} from '../../src/core/blitzkrieg/consumablesDefinitions'; TankFilterDefinitionCategory,
} from '../../src/core/blitzkrieg/consumableDefinitions';
import { import {
EquipmentDefinitions, EquipmentDefinitions,
EquipmentPreset, EquipmentPreset,
@@ -20,6 +20,7 @@ import {
ModelArmor, ModelArmor,
ModelDefinitions, ModelDefinitions,
} from '../../src/core/blitzkrieg/modelDefinitions'; } from '../../src/core/blitzkrieg/modelDefinitions';
import { ProvisionDefinitions } from '../../src/core/blitzkrieg/provisionDefinitions';
import { import {
GunDefinition, GunDefinition,
ShellType, ShellType,
@@ -61,6 +62,7 @@ type VehicleDefinitionArmor = Record<
interface VehicleDefinitions { interface VehicleDefinitions {
invisibility: { moving: number; still: number; firePenalty: number }; invisibility: { moving: number; still: number; firePenalty: number };
consumableSlots: number; consumableSlots: number;
provisionSlots: number;
optDevicePreset: string; optDevicePreset: string;
hull: { hull: {
armor: VehicleDefinitionArmor; armor: VehicleDefinitionArmor;
@@ -190,7 +192,7 @@ interface OptionalDeviceSlotRow {
[key: string]: { device0: string; device1: string }; [key: string]: { device0: string; device1: string };
} }
export interface ConsumablesCommon { export interface ConsumablesProvisionsCommon {
[key: string]: { [key: string]: {
id: number; id: number;
userString: string; userString: string;
@@ -198,7 +200,7 @@ export interface ConsumablesCommon {
icon: string; icon: string;
category: string; category: string;
tags: string; tags: string;
vehicleFilter: { vehicleFilter?: {
include: { vehicle: ConsumablesFilter }; include: { vehicle: ConsumablesFilter };
exclude?: { vehicle: ConsumablesFilter }; exclude?: { vehicle: ConsumablesFilter };
}; };
@@ -218,7 +220,7 @@ type ConsumablesFilter =
| { name: string } | { name: string }
| { extendedTags: string }; | { extendedTags: string };
const consumableEffectSuffixes = [ const consumableProvisionEffectSuffixes = [
'Increase', 'Increase',
'Factor', 'Factor',
'Bias', 'Bias',
@@ -241,7 +243,7 @@ const missingStrings: Record<string, string> = {
'#artefacts:tungstentip/name': 'Tungsten Shells', '#artefacts:tungstentip/name': 'Tungsten Shells',
}; };
export async function buildDefinitions(production: boolean) { export async function definitions(production: boolean) {
console.log('Building tank definitions...'); console.log('Building tank definitions...');
const tankDefinitions: TankDefinitions = {}; const tankDefinitions: TankDefinitions = {};
@@ -251,6 +253,7 @@ export async function buildDefinitions(production: boolean) {
equipments: {}, equipments: {},
}; };
const consumableDefinitions: ConsumableDefinitions = {}; const consumableDefinitions: ConsumableDefinitions = {};
const provisionDefinitions: ProvisionDefinitions = {};
const nations = await readdir(`${DATA}/${POI.vehicleDefinitions}`).then( const nations = await readdir(`${DATA}/${POI.vehicleDefinitions}`).then(
(nations) => nations.filter((nation) => nation !== 'common'), (nations) => nations.filter((nation) => nation !== 'common'),
); );
@@ -267,9 +270,12 @@ export async function buildDefinitions(production: boolean) {
const optionalDeviceSlots = await readXMLDVPL<{ const optionalDeviceSlots = await readXMLDVPL<{
root: OptionalDeviceSlots; root: OptionalDeviceSlots;
}>(`${DATA}/${POI.optionalDeviceSlots}.dvpl`); }>(`${DATA}/${POI.optionalDeviceSlots}.dvpl`);
const consumables = await readXMLDVPL<{ root: ConsumablesCommon }>( const consumables = await readXMLDVPL<{ root: ConsumablesProvisionsCommon }>(
`${DATA}/${POI.consumablesCommon}.dvpl`, `${DATA}/${POI.consumablesCommon}.dvpl`,
); );
const provisions = await readXMLDVPL<{ root: ConsumablesProvisionsCommon }>(
`${DATA}/${POI.provisionsCommon}.dvpl`,
);
await Promise.all( await Promise.all(
nations.map(async (nation) => { nations.map(async (nation) => {
@@ -342,6 +348,7 @@ export async function buildDefinitions(production: boolean) {
id: tankId, id: tankId,
equipment, equipment,
consumables: tankDefinition.root.consumableSlots, consumables: tankDefinition.root.consumableSlots,
provisions: tankDefinition.root.provisionSlots,
name: name:
(tank.shortUserString (tank.shortUserString
? strings[tank.shortUserString] ?? ? strings[tank.shortUserString] ??
@@ -646,9 +653,10 @@ export async function buildDefinitions(production: boolean) {
include: [], include: [],
}; };
const includeRaw = consumable.vehicleFilter.include.vehicle; const includeRaw = consumable.vehicleFilter?.include.vehicle;
const excludeRaw = consumable.vehicleFilter.exclude?.vehicle; const excludeRaw = consumable.vehicleFilter?.exclude?.vehicle;
if (includeRaw) {
if ('minLevel' in includeRaw) { if ('minLevel' in includeRaw) {
consumableDefinitions[consumable.id].include.push({ consumableDefinitions[consumable.id].include.push({
type: 'tier', type: 'tier',
@@ -661,6 +669,7 @@ export async function buildDefinitions(production: boolean) {
ids: includeRaw.name.split(' ').map((key) => tankStringIdMap[key]), ids: includeRaw.name.split(' ').map((key) => tankStringIdMap[key]),
}); });
} else throw new SyntaxError('Unhandled include type'); } else throw new SyntaxError('Unhandled include type');
}
if (excludeRaw) { if (excludeRaw) {
consumableDefinitions[consumable.id].exclude = []; consumableDefinitions[consumable.id].exclude = [];
@@ -675,7 +684,7 @@ export async function buildDefinitions(production: boolean) {
type: 'category', type: 'category',
categories: excludeRaw.extendedTags.split( categories: excludeRaw.extendedTags.split(
' ', ' ',
) as ConsumableDefinitionFilterCategory[], ) as TankFilterDefinitionCategory[],
}); });
} else throw new SyntaxError('Unhandled exclude type'); } else throw new SyntaxError('Unhandled exclude type');
} }
@@ -685,7 +694,9 @@ export async function buildDefinitions(production: boolean) {
...consumable.script.bonusValues, ...consumable.script.bonusValues,
}).forEach(([effectName, effect]) => { }).forEach(([effectName, effect]) => {
if ( if (
!consumableEffectSuffixes.some((suffix) => effectName.endsWith(suffix)) !consumableProvisionEffectSuffixes.some((suffix) =>
effectName.endsWith(suffix),
)
) { ) {
return; 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( await commitAssets(
'definitions', 'definitions',
[ [
@@ -722,6 +800,11 @@ export async function buildDefinitions(production: boolean) {
encoding: 'utf-8', encoding: 'utf-8',
path: 'definitions/consumables.json', path: 'definitions/consumables.json',
}, },
{
content: JSON.stringify(provisionDefinitions),
encoding: 'utf-8',
path: 'definitions/provisions.json',
},
], ],
production, production,
); );
+6 -6
View File
@@ -1,9 +1,9 @@
import { config } from 'dotenv'; import { config } from 'dotenv';
import { argv } from 'process'; import { argv } from 'process';
import { camouflageIcons } from './camouflageIcons'; import { camouflageIcons } from './camouflageIcons';
import { buildCircleFlags } from './circleFlags'; import { circleFlags } from './circleFlags';
import { buildConsumableIcons } from './consumableIcons'; import { consumableIcons } from './consumableIcons';
import { buildDefinitions } from './definitions'; import { definitions } from './definitions';
import { equipmentIcons } from './equipmentIcons'; import { equipmentIcons } from './equipmentIcons';
import { buildModuleIcons } from './moduleIcons'; import { buildModuleIcons } from './moduleIcons';
import { buildScratchedFlags } from './scratchedFlags'; import { buildScratchedFlags } from './scratchedFlags';
@@ -26,14 +26,14 @@ if (!targets && !allTargets) throw new Error('No target(s) specified');
const methods = [ const methods = [
buildTankModels, buildTankModels,
buildTankArmors, buildTankArmors,
buildDefinitions, definitions,
buildTankIcons, buildTankIcons,
buildScratchedFlags, buildScratchedFlags,
buildCircleFlags, circleFlags,
buildShellIcons, buildShellIcons,
buildModuleIcons, buildModuleIcons,
equipmentIcons, equipmentIcons,
buildConsumableIcons, consumableIcons,
camouflageIcons, camouflageIcons,
]; ];
@@ -1,8 +1,8 @@
import { Flex, Heading, Text } from '@radix-ui/themes'; import { Flex, Heading, Text } from '@radix-ui/themes';
import { use } from 'react'; import { use } from 'react';
import { ModuleButton } from '../../../../../../../components/ModuleButton'; import { ModuleButton } from '../../../../../../../components/ModuleButton';
import { checkConsumableInclusivity } from '../../../../../../../core/blitzkrieg/checkConsumableInclusivity'; import { checkConsumableProvisionInclusivity } from '../../../../../../../core/blitzkrieg/checkConsumableInclusivity';
import { consumableDefinitions } from '../../../../../../../core/blitzkrieg/consumablesDefinitions'; import { consumableDefinitions } from '../../../../../../../core/blitzkrieg/consumableDefinitions';
import { useDuel } from '../../../../../../../stores/duel'; import { useDuel } from '../../../../../../../stores/duel';
import { import {
mutateTankopediaTemporary, mutateTankopediaTemporary,
@@ -15,7 +15,11 @@ export function Consumables() {
const consumables = useTankopediaTemporary((state) => state.consumables); const consumables = useTankopediaTemporary((state) => state.consumables);
const consumablesList = Object.values(awaitedConsumableDefinitions).filter( const consumablesList = Object.values(awaitedConsumableDefinitions).filter(
(consumable) => (consumable) =>
checkConsumableInclusivity(consumable, protagonist.tank, protagonist.gun), checkConsumableProvisionInclusivity(
consumable,
protagonist.tank,
protagonist.gun,
),
); );
return ( return (
@@ -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 (
<Flex gap="2" direction="column">
<Heading size="4">
Provisions <Text color="gray">(max {protagonist.tank.provisions})</Text>
</Heading>
<Flex>
{provisionsList.map((provision, index) => {
const selected = provisions.includes(provision.id);
return (
<ModuleButton
first={index === 0}
last={index === provisionsList.length - 1}
rowChild
type="provision"
disabled={
protagonist.tank.provisions === provisions.length && !selected
}
provision={provision.id}
selected={selected}
onClick={() => {
mutateTankopediaTemporary((draft) => {
if (selected) {
draft.provisions = draft.provisions.filter(
(id) => id !== provision.id,
);
} else {
draft.provisions.push(provision.id);
}
});
}}
/>
);
})}
</Flex>
</Flex>
);
}
@@ -3,6 +3,7 @@ import { Camouflage } from './components/Camouflage';
import { Consumables } from './components/Consumables'; import { Consumables } from './components/Consumables';
import { Equipments } from './components/Equipments'; import { Equipments } from './components/Equipments';
import { Modules } from './components/Modules'; import { Modules } from './components/Modules';
import { Provisions } from './components/Provisions';
export function Configure() { export function Configure() {
return ( return (
@@ -14,13 +15,10 @@ export function Configure() {
<Equipments /> <Equipments />
<Consumables /> <Consumables />
<Camouflage /> <Camouflage />
<Provisions />
</Flex> </Flex>
{/* {/*
<Flex gap="4" direction="column">
<Heading size="5">Camouflage</Heading>
</Flex>
<Flex gap="4" direction="column"> <Flex gap="4" direction="column">
<Heading size="5">Provisions</Heading> <Heading size="5">Provisions</Heading>
</Flex> */} </Flex> */}
+6
View File
@@ -25,6 +25,10 @@ type ModuleButtonProps = Omit<ComponentProps<typeof Button>, 'type'> & {
type: 'consumable'; type: 'consumable';
consumable: number; consumable: number;
} }
| {
type: 'provision';
provision: number;
}
| { | {
type: 'camouflage'; type: 'camouflage';
} }
@@ -89,6 +93,8 @@ export function ModuleButton({
? `icons/consumables/${props.consumable}.webp` ? `icons/consumables/${props.consumable}.webp`
: props.type === 'camouflage' : props.type === 'camouflage'
? `icons/camo.webp` ? `icons/camo.webp`
: props.type === 'provision'
? `icons/provisions/${props.provision}.webp`
: `icons/equipment/${props.equipment}.webp`, : `icons/equipment/${props.equipment}.webp`,
)} )}
style={imageStyles} style={imageStyles}
@@ -1,12 +1,13 @@
import { ConsumableEntry } from './consumablesDefinitions'; import { ConsumableEntry } from './consumableDefinitions';
import { ProvisionEntry } from './provisionDefinitions';
import { GunDefinition, TankDefinition } from './tankDefinitions'; import { GunDefinition, TankDefinition } from './tankDefinitions';
export function checkConsumableInclusivity( export function checkConsumableProvisionInclusivity(
consumable: ConsumableEntry, consumableProvision: ConsumableEntry | ProvisionEntry,
tank: TankDefinition, tank: TankDefinition,
gun: GunDefinition, gun: GunDefinition,
) { ) {
const included = consumable.include.every((rule) => { const included = consumableProvision.include.every((rule) => {
switch (rule.type) { switch (rule.type) {
case 'tier': case 'tier':
return rule.min <= tank.tier && tank.tier <= rule.max; 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'); throw new SyntaxError('Category filtering found in include rule');
} }
}); });
const excluded = consumable.exclude?.some((rule) => { const excluded = consumableProvision.exclude?.some((rule) => {
switch (rule.type) { switch (rule.type) {
case 'tier': case 'tier':
return rule.min <= tank.tier && tank.tier <= rule.max; return rule.min <= tank.tier && tank.tier <= rule.max;
@@ -1,8 +1,8 @@
import { asset } from './asset'; import { asset } from './asset';
export type ConsumableDefinitionFilterCategory = 'clip'; export type TankFilterDefinitionCategory = 'clip';
type ConsumableDefinitionFilter = export type TankFilterDefinition =
| { | {
type: 'tier'; type: 'tier';
min: number; min: number;
@@ -14,15 +14,15 @@ type ConsumableDefinitionFilter =
} }
| { | {
type: 'category'; type: 'category';
categories: ConsumableDefinitionFilterCategory[]; categories: TankFilterDefinitionCategory[];
}; };
export interface ConsumableEntry { export interface ConsumableEntry {
id: number; id: number;
name: string; name: string;
effects?: Record<string, number>; effects?: Record<string, number>;
include: ConsumableDefinitionFilter[]; include: TankFilterDefinition[];
exclude?: ConsumableDefinitionFilter[]; exclude?: TankFilterDefinition[];
} }
export interface ConsumableDefinitions { export interface ConsumableDefinitions {
@@ -0,0 +1,21 @@
import { asset } from './asset';
import { TankFilterDefinition } from './consumableDefinitions';
export interface ProvisionEntry {
id: number;
name: string;
effects?: Record<string, number>;
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<ProvisionDefinitions>);
+1
View File
@@ -20,6 +20,7 @@ export interface TankDefinition {
name_full?: string; name_full?: string;
tree_type: TreeTypeString; tree_type: TreeTypeString;
consumables: number; consumables: number;
provisions: number;
tier: Tier; tier: Tier;
type: TankType; type: TankType;
testing?: boolean; testing?: boolean;
+2
View File
@@ -58,6 +58,7 @@ interface TankopediaTemporary {
equipmentMatrix: EquipmentMatrix; equipmentMatrix: EquipmentMatrix;
mode: TankopediaMode; mode: TankopediaMode;
consumables: number[]; consumables: number[];
provisions: number[];
camouflage: boolean; camouflage: boolean;
} }
@@ -107,6 +108,7 @@ export const useTankopediaTemporary = create<TankopediaTemporary>()(
], ],
mode: 'model', mode: 'model',
consumables: [], consumables: [],
provisions: [],
camouflage: false, camouflage: false,
})), })),
); );