From b053c37cd548a7492d20927d87b49fb3cdddf81a Mon Sep 17 00:00:00 2001 From: T14D3 Date: Thu, 18 Jul 2024 21:10:15 +0000 Subject: [PATCH] Sandbox stats --- jsconfig.json | 15 ++ src/components/ModuleGrid.css | 84 +++++++ src/components/ModuleGrid.js | 425 ++++++++++++++++++++++++++++++---- src/pages/Sandbox.css | 7 + src/pages/Sandbox.js | 12 +- src/util/api.js | 12 + 6 files changed, 506 insertions(+), 49 deletions(-) create mode 100644 jsconfig.json diff --git a/jsconfig.json b/jsconfig.json new file mode 100644 index 0000000..a3b58fe --- /dev/null +++ b/jsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "module": "ESNext", + "moduleResolution": "Bundler", + "target": "ES2020", + "jsx": "react", + "allowImportingTsExtensions": true, + "strictNullChecks": true, + "strictFunctionTypes": true + }, + "exclude": [ + "node_modules", + "**/node_modules/*" + ] +} \ No newline at end of file diff --git a/src/components/ModuleGrid.css b/src/components/ModuleGrid.css index 29b91a7..195b7cd 100644 --- a/src/components/ModuleGrid.css +++ b/src/components/ModuleGrid.css @@ -122,6 +122,90 @@ color: red; margin-top: 5px; } +.module-grid-container { + display: grid; + grid-template-columns: 2; + align-items: flex-start; + margin-top: 2em; +} +.expand-box { + margin-left: 20px; + padding: 10px; + height: 35em; + margin-top: 2em; + min-width: 25em; + background-color: rgba(40, 40, 40, 0.9); + border-radius: 15px; + grid-column: 2; + text-align: center; + +} +.stats-container { + width: 100%; + text-align: center; + border: 1px solid #3aa0ac; + border-radius: 5px; + } + + .stats-table { + width: 100%; + border-collapse: collapse; + } + + .stats-table th, .stats-table td { + border: 1px solid #3aa0ac; + padding: 8px; + text-align: left; + } + + .stats-table th { + background-color: #333; + } + +.module-drain { + display: flex; + justify-content: center; + align-items: center; + padding: 5px; + border: #3aa0ac 1px solid; + border-radius: 5px; + margin-bottom: 20px; + height: 40px; + background-color: #333; +} +.level-input { + display: flex; + justify-content: center; + align-items: center; + appearance: none; + -webkit-appearance: none; + -moz-appearance: none; + background: transparent; + border: 1px solid #3aa0ac; + border-radius: 5px; + padding: 5px; + height: 30px; + background-color: #333; + color: #3aa0ac; + margin-bottom: 20px; + width: 100%; +} +.level-input::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 15px; + height: 35px; + background: #3aa0ac; + cursor: ew-resize; + border-radius: 3px; +} +.level-input::-moz-range-thumb { + width: 15px; + height: 35px; + background: #3aa0ac; + cursor: ew-resize; + border-radius: 3px; +} .import-input { diff --git a/src/components/ModuleGrid.js b/src/components/ModuleGrid.js index 5d995a2..6ed0ca4 100644 --- a/src/components/ModuleGrid.js +++ b/src/components/ModuleGrid.js @@ -2,21 +2,71 @@ import React, { useState, useEffect } from 'react'; import GridSlot from './GridSlot'; import DraggableBox from './DraggableBox'; import Modal from './Modal'; -import { fetchModuleInfo, findDescendantData, fetchWeaponInfo } from '../util/api'; // Adjust API functions as per your implementation +import { fetchModuleInfo, findDescendantData, fetchWeaponInfo, fetchAllStats } from '../util/api'; import './ModuleGrid.css'; import Search from './Search'; -const ModuleGrid = ({ gridType, setGridType, boxes, setBoxes, moduleData, isWeaponModule, selectedDescendantId, setSelectedDescendantId, selectedWeaponId, setSelectedWeaponId }) => { +const ModuleGrid = ({ + gridType, + setGridType, + boxes, + setBoxes, + moduleData, + isWeaponModule, + selectedDescendantId, + setSelectedDescendantId, + selectedWeaponId, + setSelectedWeaponId, + showStats +}) => { const [isModalOpen, setIsModalOpen] = useState(false); const [selectedBox, setSelectedBox] = useState(null); - const [level, setLevel] = useState(1); + const [level, setLevel] = useState(0); const [minLevel, setMinLevel] = useState(0); const [maxLevel, setMaxLevel] = useState(7); const [selectedDescendantName, setSelectedDescendantName] = useState(''); const [selectedWeaponName, setSelectedWeaponName] = useState(''); const [modalModuleData, setModalModuleData] = useState(null); + const [selectedWeaponStats, setSelectedWeaponStats] = useState(null); + const [statModifiers, setStatModifiers] = useState({}); + const [weaponLevel, setWeaponLevel] = useState(100); + const [descendantLevel, setDescendantLevel] = useState(40); + const [statNameCache, setStatNameCache] = useState({}); + const [selectedDescendantStats, setSelectedDescendantStats] = useState(null); + const [totalModuleDrain, setTotalModuleDrain] = useState(0); useEffect(() => { + const initializeData = async () => { + try { + const allStats = await fetchAllStats(); + const statCache = {}; + allStats.forEach(stat => { + statCache[stat.stat_id] = stat.stat_name; + }); + setStatNameCache(statCache); + } catch (error) { + console.error('Error fetching all stats:', error); + } + }; + + initializeData(); + }, []); + + useEffect(() => { + const calculateTotalModuleDrain = () => { + const totalDrain = boxes.reduce((sum, box) => sum + (box.moduleDrain || 0), 0); + setTotalModuleDrain(totalDrain); + }; + + calculateTotalModuleDrain(); + }, [boxes]); + + + useEffect(() => { + const fetchCachedStatName = async (statId) => { + return statNameCache[statId] || null; + }; + const fetchDescendantName = async () => { if (selectedDescendantId) { try { @@ -41,9 +91,149 @@ const ModuleGrid = ({ gridType, setGridType, boxes, setBoxes, moduleData, isWeap } }; + const fetchWeaponStats = async () => { + if (!selectedWeaponId) return; + + try { + const weaponInfo = await fetchWeaponInfo(selectedWeaponId); + const statDefinitions = [ + { stat_name: 'Fire Rate', regexPattern: 'Fire Rate\\s*([+-])\\s*([\\d.]+)%' }, + { stat_name: 'Firearm Critical Hit Damage', regexPattern: 'Firearm Critical Hit Damage\\s*([+-])\\s*([\\d.]+)%' }, + { stat_name: 'Firearm Critical Hit Rate', regexPattern: 'Firearm Critical Hit Rate\\s*([+-])\\s*([\\d.]+)%' }, + { stat_name: 'Firearm ATK', regexPattern: 'Firearm ATK\\s*([+-])\\s*([\\d.]+)%' }, + { stat_name: 'Recoil', regexPattern: 'Recoil\\s*([+-])\\s*([\\d.]+)%' }, + { stat_name: 'Reload Time', regexPattern: 'Reload Time Modifier\\s*([+-])\\s*([\\d.]+)%' }, + { stat_name: 'Rounds per Magazine', regexPattern: 'Rounds per Magazine\\s*([+-])\\s*([\\d.]+)%' }, + { stat_name: 'Accuracy', regexPattern: 'Accuracy\\s*([+-])\\s*([\\d.]+)%' }, + ]; + + const levelEntry = weaponInfo.firearm_atk.find(entry => entry.level === parseInt(weaponLevel)); + const weaponAtk = levelEntry ? levelEntry.firearm[0].firearm_atk_value : 0; + + // Adding the manual Firearm ATK stat + weaponInfo.base_stat.push({ + stat_id: '105000026', + stat_name: 'Firearm ATK', + stat_value: weaponAtk + }); + + // Fetch and assign stat names if not present + for (const stat of weaponInfo.base_stat) { + if (!stat.stat_name) { + try { + stat.stat_name = await fetchCachedStatName(stat.stat_id); + } catch (error) { + console.error(`Error fetching stat name for stat ID ${stat.stat_id}:`, error); + } + } + } + + const filteredStats = weaponInfo.base_stat.filter(stat => + statDefinitions.some(def => def.stat_name === stat.stat_name) + ); + + const statModifiers = await Promise.all( + boxes.map(async box => { + const moduleInfo = await fetchModuleInfo(box.moduleId); + const moduleDesc = moduleInfo.module_stat[box.level]?.value || ''; + return parseWeaponModuleDescriptions(moduleDesc, statDefinitions); + }) + ); + + const combinedModifiers = statModifiers.reduce((accumulator, currentValue) => { + Object.keys(currentValue).forEach(stat_name => { + if (!accumulator[stat_name]) { + accumulator[stat_name] = 1; + } + accumulator[stat_name] *= 1 + currentValue[stat_name] / 100; + }); + return accumulator; + }, {}); + + const modifiedStats = filteredStats.map(baseStat => { + if (combinedModifiers[baseStat.stat_name]) { + return { + ...baseStat, + stat_value: baseStat.stat_value * combinedModifiers[baseStat.stat_name] + }; + } + return baseStat; + }); + + setSelectedWeaponStats(modifiedStats); + setStatModifiers(combinedModifiers); + } catch (error) { + console.error(`Error fetching weapon info for ID ${selectedWeaponId}:`, error); + setSelectedWeaponStats(null); + } + }; + const fetchDescendantStats = async () => { + if (selectedDescendantId) { + try { + const descendantInfo = await findDescendantData(selectedDescendantId); + const statDefinitions = [ + { stat_type: 'Max HP', regexPattern: 'Max HP\\s*([+-])\\s*([\\d.]+)%' }, + { stat_type: 'Max Shield', regexPattern: 'Max Shield\\s*([+-])\\s*([\\d.]+)%' }, + { stat_type: 'Max MP', regexPattern: 'Max MP\\s*([+-])\\s*([\\d.]+)%' }, + { stat_type: 'DEF', regexPattern: 'DEF\\s*([+-])\\s*([\\d.]+)%' }, + { stat_type: 'Shield Recovery Out of Combat', regexPattern: 'Shield Recovery Out of Combat\\s*([+-])\\s*([\\d.]+)%' }, + { stat_type: 'Shield Recovery In Combat', regexPattern: 'Shield Recovery In Combat\\s*([+-])\\s*([\\d.]+)%' }, + ]; + + const levelEntry = descendantInfo.descendant_stat.find(entry => entry.level === parseInt(descendantLevel)); + console.log('levelEntry', levelEntry); + const baseStats = levelEntry ? levelEntry.stat_detail : []; + console.log('baseStats', baseStats); + + + + + + const statModifiers = await Promise.all( + boxes.map(async box => { + const moduleInfo = await fetchModuleInfo(box.moduleId); + const moduleDesc = moduleInfo.module_stat[box.level]?.value || ''; + const values = parseDescendantModuleDescriptions + (moduleDesc, statDefinitions); + console.log('moduleDesc', moduleDesc); + console.log('values', values); + return values; + }) + ); + + const combinedModifiers = statModifiers.reduce((accumulator, currentValue) => { + Object.keys(currentValue).forEach(stat_type => { + if (!accumulator[stat_type]) { + accumulator[stat_type] = 1; + } + accumulator[stat_type] *= 1 + currentValue[stat_type] / 100; + }); + return accumulator; + }, {}); + + const modifiedStats = baseStats.map(baseStat => { + if (combinedModifiers[baseStat.stat_type]) { + return { + ...baseStat, + stat_value: baseStat.stat_value * combinedModifiers[baseStat.stat_type] + }; + } + return baseStat; + }); + + setSelectedDescendantStats(modifiedStats); + setStatModifiers(combinedModifiers); + } catch (error) { + console.error(`Error fetching descendant info for ID ${selectedDescendantId}:`, error); + setSelectedDescendantStats(null); + } + } + }; + fetchDescendantStats(); + fetchWeaponStats(); fetchDescendantName(); fetchWeaponName(); - }, [selectedDescendantId, selectedWeaponId]); + }, [selectedDescendantId, selectedWeaponId, boxes, weaponLevel, statNameCache, descendantLevel]); const addBox = async (searchData) => { try { @@ -52,13 +242,10 @@ const ModuleGrid = ({ gridType, setGridType, boxes, setBoxes, moduleData, isWeap if (currentGrid.some(box => box.moduleId === searchData.id)) { throw new Error('Module already added.'); } - const moduleInfo = await fetchModuleInfo(searchData.id); - const defaultLevel = 0; const firstModuleStat = moduleInfo.module_stat[0]; const moduleDrain = firstModuleStat.module_capacity; - const newBox = { id: currentGrid.length + 1, slot: findFirstFreeSlot(currentGrid), @@ -70,7 +257,6 @@ const ModuleGrid = ({ gridType, setGridType, boxes, setBoxes, moduleData, isWeap moduleStats: moduleInfo.module_stat, moduleDrain: moduleDrain, }; - console.log(`New box added to ${gridType} grid:`, newBox); setBoxes([...currentGrid, newBox]); } else if (searchData.searchType === 'descendant') { @@ -111,6 +297,39 @@ const ModuleGrid = ({ gridType, setGridType, boxes, setBoxes, moduleData, isWeap return Array.from({ length: 12 }, (_, i) => i).find(slot => !occupiedSlots.includes(slot)); }; + const parseWeaponModuleDescriptions = (description, statDefinitions) => { + const values = {}; + + statDefinitions.forEach(({ stat_name, regexPattern }) => { + const regex = new RegExp(regexPattern, 'i'); + const match = description.match(regex); + if (match) { + const sign = match[1] === '+' ? 1 : -1; + values[stat_name] = sign * parseFloat(match[2]); + } else { + values[stat_name] = 0; + } + }); + + return values; + }; + const parseDescendantModuleDescriptions = (description, statDefinitions) => { + const values = {}; + + statDefinitions.forEach(({ stat_type, regexPattern }) => { + const regex = new RegExp(regexPattern, 'i'); + const match = description.match(regex); + if (match) { + const sign = match[1] === '+' ? 1 : -1; + values[stat_type] = sign * parseFloat(match[2]); + } else { + values[stat_type] = 0; + } + }); + + return values; + }; + const handleBoxClick = async (box) => { try { const moduleInfo = await fetchModuleInfo(box.moduleId); @@ -155,48 +374,160 @@ const ModuleGrid = ({ gridType, setGridType, boxes, setBoxes, moduleData, isWeap handleModalClose(); }; + const renderWeaponStats = (statDefinitions) => { + if (!selectedWeaponStats) { + return

No weapon stats available.

; + } + + const filteredStats = selectedWeaponStats.filter(stat => + statDefinitions.some(def => def.stat_name === stat.stat_name) + ); + + if (filteredStats.length === 0) { + return

No matching stats found.

; + } + + return ( +
+ + + + + + + + + {filteredStats.map(stat => ( + + + + + ))} + +
Stat NameStat Value
{stat.stat_name}{stat.stat_value.toFixed(2)}
+
+ ); + }; + const renderDescendantStats = (statDefinitions) => { + if (!selectedDescendantStats) { + return

No descendant stats available.

; + } + + const filteredStats = selectedDescendantStats.filter(stat => + statDefinitions.some(def => def.stat_type === stat.stat_type) + ); + + if (filteredStats.length === 0) { + return

No matching stats found.

; + } + + return ( +
+ + + + + + + + + {filteredStats.map(stat => ( + + + + + ))} + +
Stat TypeStat Value
{stat.stat_type}{stat.stat_value.toFixed(2)}
+
+ ); + }; + + return ( -
-
-

{gridType}

- {!isWeaponModule && ( -
- addBox(searchData)} /> - addBox(searchData)} /> -

{selectedDescendantName || 'No Descendant selected'}

-
+
+
+
+

{gridType}

+ {!isWeaponModule && ( +
+ addBox(searchData)} /> + addBox(searchData)} /> +

{selectedDescendantName || 'No Descendant selected'}

+
+ )} + {isWeaponModule && ( +
+ addBox(searchData)} /> + addBox(searchData)} /> +

{selectedWeaponName || 'No Weapon selected'}

+
+ )} +
+
+ {Array.from({ length: 12 }, (_, i) => i).map(slot => ( + moveBox(boxId, newSlot)}> + {boxes + .filter(box => box.slot === slot) + .map(box => ( + moveBox(boxId, newSlot)} onClick={() => handleBoxClick(box)} /> + ))} + + ))} +
+ {selectedBox && ( + )} - {isWeaponModule && ( -
- addBox(searchData)} /> - addBox(searchData)} /> -

{selectedWeaponName || 'No Weapon selected'}

-
- )} -
-
- {Array.from({ length: 12 }, (_, i) => i).map(slot => ( - moveBox(boxId, newSlot)}> - {boxes - .filter(box => box.slot === slot) - .map(box => ( - moveBox(boxId, newSlot)} onClick={() => handleBoxClick(box)} /> - ))} - - ))}
- {selectedBox && ( - + {showStats && isWeaponModule && ( +
+
+

Module Drain:

70 ? 'red' : 'green'}}> + {totalModuleDrain}/70 +

+ setWeaponLevel(e.target.value)} /> +

Weapon Level: {weaponLevel}

+

Weapon Stats:

+ {renderWeaponStats([ + { stat_name: 'Fire Rate' }, + { stat_name: 'Firearm Critical Hit Damage' }, + { stat_name: 'Firearm Critical Hit Rate' }, + { stat_name: 'Firearm ATK' }, + { stat_name: 'Recoil' }, + { stat_name: 'Reload Time' }, + { stat_name: 'Rounds per Magazine' }, + { stat_name: 'Accuracy' }, + ])} +
+ )} + {showStats && !isWeaponModule && ( +
+
+

Module Drain:

70 ? 'red' : 'green'}}> + {totalModuleDrain}/70 +

+ setDescendantLevel(e.target.value)} /> +

Level: {descendantLevel}

+

Descendant Stats:

+ {renderDescendantStats([ + { stat_type: 'Max HP'}, + { stat_type: 'Max Shield'}, + { stat_type: 'Max MP'}, + { stat_type: 'DEF'}, + { stat_type: 'Shield Recovery Out of Combat'}, + { stat_type: 'Shield Recovery In Combat'}, + ])} +
)}
); diff --git a/src/pages/Sandbox.css b/src/pages/Sandbox.css index 675d771..062b633 100755 --- a/src/pages/Sandbox.css +++ b/src/pages/Sandbox.css @@ -17,12 +17,19 @@ body { padding: 10px; background-color: rgba(40, 40, 40, 0.9); border-radius: 15px; + height: 35em; } .error-message { color: red; margin-top: 5px; } +.sandbox-header { + display: flex; + justify-content: space-between; + align-items: center; +} +.stats-button, .save-button { padding: 10px; font-size: 20px; diff --git a/src/pages/Sandbox.js b/src/pages/Sandbox.js index a101897..6ca69f2 100755 --- a/src/pages/Sandbox.js +++ b/src/pages/Sandbox.js @@ -28,7 +28,8 @@ const Sandbox = () => { const [gridType3, setGridType3] = useState("Impact Rounds"); const [gridType4, setGridType4] = useState("High-Power Rounds"); - const [buildId, setBuildId] = useState(null); // State to hold the build ID + const [buildId, setBuildId] = useState(null); + const [showStats, setShowStats] = useState(false); const location = useLocation(); const navigate = useNavigate(); @@ -185,8 +186,11 @@ const Sandbox = () => {
- + +