From d5358f1ea21209ab51c5bbfb699343999f100d1f Mon Sep 17 00:00:00 2001 From: NindoK Date: Wed, 29 Mar 2023 12:35:20 +0200 Subject: [PATCH 01/10] Solving issue #1905: Aggregate Fights into a single txn to reduce gas fees --- README.md | 13 +- contracts/TokensManager.sol | 72 ++- frontend/src/store/combat/index.ts | 409 +++++++++--- frontend/src/views/Combat.vue | 981 ++++++++++++++++++----------- 4 files changed, 981 insertions(+), 494 deletions(-) diff --git a/README.md b/README.md index 0b2bdc010..2e3a80468 100644 --- a/README.md +++ b/README.md @@ -7,12 +7,12 @@ This code is open, but not open source. It is not licensed, which means you cann ## Currency Setup 1. Install [Ganache](https://www.trufflesuite.com/ganache). -1. For Ganache, choose Quickstart Ethereum. -1. Increase the gas limit in the workspace to `99999999` (or some other high number so you can deploy). -1. Install [MetaMask](https://metamask.io/). -1. Create a new connection to connect to Ganache with these settings: http://localhost:7545, any name, any chain id -1. In Ganache, click the key icon on the right side of any address and grab the private key. -1. In MetaMask, create a new account, import from private key, and paste the key in there. +2. For Ganache, choose Quickstart Ethereum. +3. Increase the gas limit in the workspace to `99999999` (or some other high number so you can deploy). +4. Install [MetaMask](https://metamask.io/). +5. Create a new connection to connect to Ganache with these settings: http://localhost:7545, any name, any chain id +6. In Ganache, click the key icon on the right side of any address and grab the private key. Make sure that chainID is 5777. +7. In MetaMask, create a new account, import from private key, and paste the key in there. You should now have 100 fake eth! You're now fake rich. @@ -118,7 +118,6 @@ If you get any issues during deployment, run: - Language is loaded on startup and added to the language drop-down of the Options page. - The value for the drop-down is "name" at the root of the json map. - ### i18n Manager App - Adding translations is easier with the use of [i18n-manager](https://www.electronjs.org/apps/i18n-manager) diff --git a/contracts/TokensManager.sol b/contracts/TokensManager.sol index a3b5ac448..05584c725 100644 --- a/contracts/TokensManager.sol +++ b/contracts/TokensManager.sol @@ -28,9 +28,7 @@ contract TokensManager is Initializable, AccessControlUpgradeable { require(hasRole(GAME_ADMIN, msg.sender)); } - function initialize( - address gameContract - ) public initializer { + function initialize(address gameContract) public initializer { __AccessControl_init_unchained(); _setupRole(DEFAULT_ADMIN_ROLE, msg.sender); @@ -39,25 +37,73 @@ contract TokensManager is Initializable, AccessControlUpgradeable { offsetSlippage = 5; } - receive() external payable restricted { - } + receive() external payable restricted {} + + function fight( + uint256 char, + uint32 target, + uint8 fightMultiplier + ) external payable { + uint256 totalTokens; + uint256 totalExpectedTokens; + for (uint256 i = 0; i < char.length; i++) { + (uint256 tokens, uint256 expectedTokens) = game.fight( + msg.sender, + char[i], + target[i], + fightMultiplier[i] + ); + totalTokens += tokens; + totalExpectedTokens += expectedTokens; + } + //The following can be put outside the loop? I'm not 100% sure this is correct + uint256 offset = ABDKMath64x64.mulu( + getSkillToNativeRatio(), + totalExpectedTokens.mul(combatTokenChargePercent).div(100) + ); - function fight(uint256 char, uint32 target, uint8 fightMultiplier) external payable { - (uint256 tokens, uint256 expectedTokens) = game.fight(msg.sender, char, target, fightMultiplier); + require( + msg.value >= offset.mul(100 - offsetSlippage).div(100) && + msg.value <= offset.mul(100 + offsetSlippage).div(100), + "Offset error" + ); + if (totalTokens == 0) { + payable(msg.sender).transfer(msg.value); + } + } - uint256 offset = ABDKMath64x64.mulu(getSkillToNativeRatio(), expectedTokens.mul(combatTokenChargePercent).div(100)); + function singleFight( + uint256 char, + uint32 target, + uint8 fightMultiplier + ) external payable { + (uint256 tokens, uint256 expectedTokens) = game.fight( + msg.sender, + char, + target, + fightMultiplier + ); + + uint256 offset = ABDKMath64x64.mulu( + getSkillToNativeRatio(), + expectedTokens.mul(combatTokenChargePercent).div(100) + ); require( - msg.value >= offset.mul(100 - offsetSlippage).div(100) && msg.value <= offset.mul(100 + offsetSlippage).div(100), - 'Offset error' - ); + msg.value >= offset.mul(100 - offsetSlippage).div(100) && + msg.value <= offset.mul(100 + offsetSlippage).div(100), + "Offset error" + ); if (tokens == 0) { payable(msg.sender).transfer(msg.value); } } - function retrieve(address addressToTransferTo, uint256 amount) external restricted { + function retrieve( + address addressToTransferTo, + uint256 amount + ) external restricted { payable(addressToTransferTo).transfer(amount); } @@ -80,4 +126,4 @@ contract TokensManager is Initializable, AccessControlUpgradeable { function setOffsetSlippage(uint8 slippage) external restricted { offsetSlippage = slippage; } -} \ No newline at end of file +} diff --git a/frontend/src/store/combat/index.ts b/frontend/src/store/combat/index.ts index 1b1f15d7a..818c79356 100644 --- a/frontend/src/store/combat/index.ts +++ b/frontend/src/store/combat/index.ts @@ -1,13 +1,13 @@ -import _ from 'lodash'; -import {gasUsedToBnb} from '@/utils/common'; -import {IState, ITarget} from '@/interfaces'; -import {isUndefined} from 'lodash'; -import {targetFromContract} from '@/contract-models'; -import {Dispatch, Commit} from 'vuex'; -import { getGasPrice } from '../store'; -import BigNumber from 'bignumber.js'; -import Vue from 'vue'; -import { getFeeInSkillFromUsd } from '@/contract-call-utils'; +import _ from "lodash"; +import { gasUsedToBnb } from "@/utils/common"; +import { IState, ITarget } from "@/interfaces"; +import { isUndefined } from "lodash"; +import { targetFromContract } from "@/contract-models"; +import { Dispatch, Commit } from "vuex"; +import { getGasPrice } from "../store"; +import BigNumber from "bignumber.js"; +import Vue from "vue"; +import { getFeeInSkillFromUsd } from "@/contract-call-utils"; export interface ICombatState { isInCombat: boolean; @@ -16,15 +16,17 @@ export interface ICombatState { fightGasOffset: string; } -const defaultCallOptions = (rootState: IState) => ({ from: rootState.defaultAccount }); +const defaultCallOptions = (rootState: IState) => ({ + from: rootState.defaultAccount, +}); const combat = { namespaced: true, state: { isInCombat: false, targetsByCharacterId: {}, - fightBaseline: '0', - fightGasOffset: '0', + fightBaseline: "0", + fightGasOffset: "0", } as ICombatState, getters: { getTargetsByCharacterId(state: ICombatState) { @@ -50,137 +52,293 @@ const combat = { setIsInCombat(state: ICombatState, isInCombat: boolean) { state.isInCombat = isInCombat; }, - updateTargets(state: ICombatState, { characterId, targets }: {characterId: number, targets: ITarget[]}) { + updateTargets( + state: ICombatState, + { characterId, targets }: { characterId: number; targets: ITarget[] } + ) { if (!state.targetsByCharacterId[characterId]) { Vue.set(state.targetsByCharacterId, characterId, {}); } Vue.set(state.targetsByCharacterId, characterId, targets); }, - updateFightBaseline(state: ICombatState, { fightBaseline }: { fightBaseline: string }) { + updateFightBaseline( + state: ICombatState, + { fightBaseline }: { fightBaseline: string } + ) { state.fightBaseline = fightBaseline; }, - updateFightGasOffset(state: ICombatState, { fightGasOffset }: { fightGasOffset: string }) { + updateFightGasOffset( + state: ICombatState, + { fightGasOffset }: { fightGasOffset: string } + ) { state.fightGasOffset = fightGasOffset; }, }, actions: { - async fetchIgoRewardsPerFight({ rootState }: {rootState: IState}) { + async fetchIgoRewardsPerFight({ rootState }: { rootState: IState }) { const { CryptoBlades } = rootState.contracts(); - if(!CryptoBlades || !rootState.defaultAccount) return; + if (!CryptoBlades || !rootState.defaultAccount) return; const IGO = 28; return await CryptoBlades.methods .vars(IGO) .call(defaultCallOptions(rootState)); }, - async fetchGasOffsetPerFight({ rootState }: {rootState: IState}) { + async fetchGasOffsetPerFight({ rootState }: { rootState: IState }) { const { CryptoBlades } = rootState.contracts(); - if(!CryptoBlades || !rootState.defaultAccount) return; + if (!CryptoBlades || !rootState.defaultAccount) return; return await CryptoBlades.methods .vars(27) .call(defaultCallOptions(rootState)); }, - async fetchHourlyPowerAverage({ rootState }: {rootState: IState}) { + async fetchHourlyPowerAverage({ rootState }: { rootState: IState }) { const { CryptoBlades } = rootState.contracts(); - if(!CryptoBlades) return; - return await CryptoBlades.methods.vars(4).call(defaultCallOptions(rootState)); + if (!CryptoBlades) return; + return await CryptoBlades.methods + .vars(4) + .call(defaultCallOptions(rootState)); }, - async fetchHourlyPayPerFight({ rootState }: {rootState: IState}) { + async fetchHourlyPayPerFight({ rootState }: { rootState: IState }) { const { CryptoBlades } = rootState.contracts(); - if(!CryptoBlades) return; - return await CryptoBlades.methods.vars(5).call(defaultCallOptions(rootState)); + if (!CryptoBlades) return; + return await CryptoBlades.methods + .vars(5) + .call(defaultCallOptions(rootState)); }, - async fetchHourlyAllowance({ rootState }: {rootState: IState}) { + async fetchHourlyAllowance({ rootState }: { rootState: IState }) { const { CryptoBlades } = rootState.contracts(); - if(!CryptoBlades) return; - return await CryptoBlades.methods.vars(18).call(defaultCallOptions(rootState)); + if (!CryptoBlades) return; + return await CryptoBlades.methods + .vars(18) + .call(defaultCallOptions(rootState)); }, - async fetchTargets({rootState, commit}: {rootState: IState, commit: Commit}, { characterId }: {characterId: number }) { - if(isUndefined(characterId)) { - commit('updateTargets', { characterId, targets: [] }); + async fetchTargets( + { rootState, commit }: { rootState: IState; commit: Commit }, + { characterId }: { characterId: number } + ) { + if (isUndefined(characterId)) { + commit("updateTargets", { characterId, targets: [] }); return; } - const targets = await rootState.contracts().CryptoBlades!.methods - .getTargets(characterId) + const targets = await rootState + .contracts() + .CryptoBlades!.methods.getTargets(characterId) .call(defaultCallOptions(rootState)); - commit('updateTargets', { characterId, targets: targets.map(targetFromContract) }); + commit("updateTargets", { + characterId, + targets: targets.map(targetFromContract), + }); }, - async getCharacterPower({rootState}: {rootState: IState}, characterId: number) { + async getCharacterPower( + { rootState }: { rootState: IState }, + characterId: number + ) { const { Characters } = rootState.contracts(); if (!Characters || !rootState.defaultAccount) return; - return await Characters.methods.getPower(characterId).call({from: rootState.defaultAccount, gasPrice: getGasPrice()}); + return await Characters.methods + .getPower(characterId) + .call({ from: rootState.defaultAccount, gasPrice: getGasPrice() }); }, async fetchExpectedPayoutForMonsterPower( - { rootState }: {rootState: IState}, { power }: {power: string | number}) { + { rootState }: { rootState: IState }, + { power }: { power: string | number } + ) { const { CryptoBlades } = rootState.contracts(); - if(!CryptoBlades) return; - return await CryptoBlades.methods.getTokenGainForFight(power).call(defaultCallOptions(rootState)); + if (!CryptoBlades) return; + return await CryptoBlades.methods + .getTokenGainForFight(power) + .call(defaultCallOptions(rootState)); }, - async getNativeTokenPriceInUsd({ rootState }: {rootState: IState}) { + async getNativeTokenPriceInUsd({ rootState }: { rootState: IState }) { const { TokensManager } = rootState.contracts(); if (!TokensManager || !rootState.defaultAccount) return; - return await TokensManager.methods.tokenPrice().call(defaultCallOptions(rootState)); + return await TokensManager.methods + .tokenPrice() + .call(defaultCallOptions(rootState)); }, - async getCurrentSkillPrice({ rootState }: {rootState: IState}) { + async getCurrentSkillPrice({ rootState }: { rootState: IState }) { const { TokensManager } = rootState.contracts(); if (!TokensManager || !rootState.defaultAccount) return; - return await TokensManager.methods.skillTokenPrice().call(defaultCallOptions(rootState)); + return await TokensManager.methods + .skillTokenPrice() + .call(defaultCallOptions(rootState)); }, async doEncounterPayNative( - { rootState, dispatch }: {rootState: IState, dispatch: Dispatch}, - { characterId, targetString, fightMultiplier, offsetCost }: - { characterId: number, targetString: number, fightMultiplier: number, offsetCost: BigNumber }) { + { rootState, dispatch }: { rootState: IState; dispatch: Dispatch }, + { + characterId, + targetString, + fightMultiplier, + offsetCost, + }: { + characterId: number; + targetString: number; + fightMultiplier: number; + offsetCost: BigNumber; + } + ) { const { TokensManager, CryptoBlades } = rootState.contracts(); if (!TokensManager || !CryptoBlades || !rootState.defaultAccount) return; const res = await TokensManager.methods - .fight( - characterId, - targetString, - fightMultiplier - ) - .send({ from: rootState.defaultAccount, gasPrice: getGasPrice(), gas: '300000', value: +offsetCost * fightMultiplier }); - - let playerRoll = ''; - let enemyRoll = ''; + .fight(characterId, targetString, fightMultiplier) + .send({ + from: rootState.defaultAccount, + gasPrice: getGasPrice(), + gas: "300000", + value: +offsetCost * fightMultiplier, + }); + + let playerRoll = ""; + let enemyRoll = ""; let xpGain; let skillGain; - const fightOutcomeEvents = await CryptoBlades.getPastEvents('FightOutcome', { - filter: { owner: rootState.defaultAccount!, character: characterId }, - toBlock: res.blockNumber, - fromBlock: res.blockNumber - }); + const fightOutcomeEvents = await CryptoBlades.getPastEvents( + "FightOutcome", + { + filter: { owner: rootState.defaultAccount!, character: characterId }, + toBlock: res.blockNumber, + fromBlock: res.blockNumber, + } + ); if (fightOutcomeEvents.length) { - playerRoll = fightOutcomeEvents[fightOutcomeEvents.length - 1].returnValues.playerRoll; - enemyRoll = fightOutcomeEvents[fightOutcomeEvents.length - 1].returnValues.enemyRoll; - xpGain = fightOutcomeEvents[fightOutcomeEvents.length - 1].returnValues.xpGain; - skillGain = fightOutcomeEvents[fightOutcomeEvents.length - 1].returnValues.skillGain; + playerRoll = + fightOutcomeEvents[fightOutcomeEvents.length - 1].returnValues + .playerRoll; + enemyRoll = + fightOutcomeEvents[fightOutcomeEvents.length - 1].returnValues + .enemyRoll; + xpGain = + fightOutcomeEvents[fightOutcomeEvents.length - 1].returnValues.xpGain; + skillGain = + fightOutcomeEvents[fightOutcomeEvents.length - 1].returnValues + .skillGain; } - const {gasPrice} = await rootState.web3.eth.getTransaction(res.transactionHash); + const { gasPrice } = await rootState.web3.eth.getTransaction( + res.transactionHash + ); const bnbGasUsed = gasUsedToBnb(res.gasUsed, gasPrice); - await Promise.all([ - dispatch('combat/fetchTargets', {characterId}), - ]); + await Promise.all([dispatch("combat/fetchTargets", { characterId })]); + + return { + isVictory: parseInt(playerRoll, 10) >= parseInt(enemyRoll, 10), + playerRoll, + enemyRoll, + xpGain, + skillGain, + bnbGasUsed, + }; + }, + + async doEncountersPayNative( + { rootState, dispatch }: { rootState: IState; dispatch: Dispatch }, + { + charactersId, + targetsString, + fightMultiplier, + offsetCost, + }: { + charactersId: number[]; + targetsString: number[]; + fightMultiplier: number[]; + offsetCost: BigNumber; + } + ) { + const { TokensManager, CryptoBlades } = rootState.contracts(); + if (!TokensManager || !CryptoBlades || !rootState.defaultAccount) return; + let multiplier: string[] = []; + let characters: string[] = []; + let targets: string[] = []; + let totalOffset: BigNumber = offsetCost; + for (var i = 0; i < targetsString.length; i++) { + characters.push(charactersId[i].toString()); + targets.push(targetsString[i].toString()); + multiplier.push(fightMultiplier[i].toString()); + totalOffset = totalOffset.multipliedBy(fightMultiplier[i]); + } + + const res = await TokensManager.methods + .fight(characters, targets, multiplier) + .call({ + from: rootState.defaultAccount, + gasPrice: getGasPrice(), + gas: "800000", + //TODO this should have all the fightMultipliers + value: +offsetCost * fightMultiplier[0], + }); + + let playerRoll = ""; + let enemyRoll = ""; + let xpGain = 0; + let skillGain = 0; + + for (var i = 0; i < characters.length; i++) { + const fightOutcomeEvents = await CryptoBlades.getPastEvents( + "FightOutcome", + { + filter: { + owner: rootState.defaultAccount!, + character: charactersId[i], + }, + toBlock: res.blockNumber, + fromBlock: res.blockNumber, + } + ); + + if (fightOutcomeEvents.length) { + playerRoll += + fightOutcomeEvents[fightOutcomeEvents.length - 1].returnValues + .playerRoll; + enemyRoll += + fightOutcomeEvents[fightOutcomeEvents.length - 1].returnValues + .enemyRoll; + + xpGain += parseInt( + fightOutcomeEvents[fightOutcomeEvents.length - 1].returnValues + .xpGain + ); + + skillGain += parseInt( + fightOutcomeEvents[fightOutcomeEvents.length - 1].returnValues + .skillGain + ); + } + + if (i < charactersId.length - 1) { + playerRoll += ", "; + enemyRoll += ", "; + } + } + + const { gasPrice } = await rootState.web3.eth.getTransaction( + res.transactionHash + ); + + const bnbGasUsed = gasUsedToBnb(res.gasUsed, gasPrice); + + for (var i = 0; i < charactersId.length; i++) { + await dispatch("combat/fetchTargets", { characterId: charactersId[i] }); + } return { isVictory: parseInt(playerRoll, 10) >= parseInt(enemyRoll, 10), @@ -188,22 +346,28 @@ const combat = { enemyRoll, xpGain, skillGain, - bnbGasUsed + bnbGasUsed, }; }, - async getCombatTokenChargePercent({ rootState }: {rootState: IState}) { + async getCombatTokenChargePercent({ rootState }: { rootState: IState }) { const { TokensManager } = rootState.contracts(); - if(!TokensManager || !rootState.defaultAccount) return; + if (!TokensManager || !rootState.defaultAccount) return; return await TokensManager.methods .combatTokenChargePercent() .call(defaultCallOptions(rootState)); }, - async fetchFightRewardSkill({ rootState, commit }: {rootState: IState, commit: Commit}) { + async fetchFightRewardSkill({ + rootState, + commit, + }: { + rootState: IState; + commit: Commit; + }) { const { CryptoBlades } = rootState.contracts(); - if(!CryptoBlades) return; + if (!CryptoBlades) return; const [skillRewards] = await Promise.all([ (async () => { @@ -211,19 +375,25 @@ const combat = { .getTokenRewards() .call(defaultCallOptions(rootState)); - commit('updateSkillRewards', { skillRewards }, { root: true }); + commit("updateSkillRewards", { skillRewards }, { root: true }); return skillRewards; - })() + })(), ]); return skillRewards; }, - async fetchFightRewardValor({ rootState, commit }: {rootState: IState, commit: Commit}) { + async fetchFightRewardValor({ + rootState, + commit, + }: { + rootState: IState; + commit: Commit; + }) { const { CryptoBlades } = rootState.contracts(); const defaultAccount = rootState.defaultAccount; - if(!CryptoBlades || !defaultAccount) return; + if (!CryptoBlades || !defaultAccount) return; const [valorRewards] = await Promise.all([ (async () => { @@ -231,76 +401,113 @@ const combat = { .userVars(defaultAccount, 10011) .call(defaultCallOptions(rootState)); - commit('updateValorRewards', { valorRewards }, { root: true }); + commit("updateValorRewards", { valorRewards }, { root: true }); return valorRewards; - })() + })(), ]); return valorRewards; }, - async fetchFightRewardXp({ rootState, commit }: {rootState: IState, commit: Commit}) { + async fetchFightRewardXp({ + rootState, + commit, + }: { + rootState: IState; + commit: Commit; + }) { const { CryptoBlades } = rootState.contracts(); - if(!CryptoBlades) return; + if (!CryptoBlades) return; - const xps = await CryptoBlades.methods.getXpRewards(rootState.ownedCharacterIds.map(x => x.toString())).call(defaultCallOptions(rootState)); + const xps = await CryptoBlades.methods + .getXpRewards(rootState.ownedCharacterIds.map((x) => x.toString())) + .call(defaultCallOptions(rootState)); const xpCharaIdPairs = rootState.ownedCharacterIds.map((charaId, i) => { return [charaId, xps[i]]; }); - commit('updateXpRewards', { xpRewards: _.fromPairs(xpCharaIdPairs) }, { root: true }); + commit( + "updateXpRewards", + { xpRewards: _.fromPairs(xpCharaIdPairs) }, + { root: true } + ); return xpCharaIdPairs; }, - async fetchCharacterStamina({ rootState, commit }: {rootState: IState, commit: Commit}, characterId: number) { - const staminaString = await rootState.contracts().Characters!.methods - .getStaminaPoints('' + characterId) + async fetchCharacterStamina( + { rootState, commit }: { rootState: IState; commit: Commit }, + characterId: number + ) { + const staminaString = await rootState + .contracts() + .Characters!.methods.getStaminaPoints("" + characterId) .call(defaultCallOptions(rootState)); const stamina = parseInt(staminaString, 10); if (rootState.characterStaminas[characterId] !== stamina) { - commit('updateCharacterStamina', { characterId, stamina }, { root: true }); + commit( + "updateCharacterStamina", + { characterId, stamina }, + { root: true } + ); } }, - async fetchFightGasOffset({ rootState, commit }: {rootState: IState, commit: Commit}) { + async fetchFightGasOffset({ + rootState, + commit, + }: { + rootState: IState; + commit: Commit; + }) { const { CryptoBlades } = rootState.contracts(); - if(!CryptoBlades) return; + if (!CryptoBlades) return; const fightGasOffset = await getFeeInSkillFromUsd( CryptoBlades, defaultCallOptions(rootState), - cryptoBladesMethods => cryptoBladesMethods.fightRewardGasOffset() + (cryptoBladesMethods) => cryptoBladesMethods.fightRewardGasOffset() ); - commit('updateFightGasOffset', { fightGasOffset }); + commit("updateFightGasOffset", { fightGasOffset }); return fightGasOffset; }, - async fetchFightBaseline({ rootState, commit }: {rootState: IState, commit: Commit}) { + async fetchFightBaseline({ + rootState, + commit, + }: { + rootState: IState; + commit: Commit; + }) { const { CryptoBlades } = rootState.contracts(); - if(!CryptoBlades) return; + if (!CryptoBlades) return; const fightBaseline = await getFeeInSkillFromUsd( CryptoBlades, defaultCallOptions(rootState), - cryptoBladesMethods => cryptoBladesMethods.fightRewardBaseline() + (cryptoBladesMethods) => cryptoBladesMethods.fightRewardBaseline() ); - commit('updateFightBaseline', { fightBaseline }); + commit("updateFightBaseline", { fightBaseline }); return fightBaseline; }, - async getFightXpGain({rootState}: {rootState: IState}) { - const {CryptoBlades} = rootState.contracts(); + async getFightXpGain({ rootState }: { rootState: IState }) { + const { CryptoBlades } = rootState.contracts(); if (!CryptoBlades) return; - return +await CryptoBlades.methods.fightXpGain().call(defaultCallOptions(rootState)); + return +(await CryptoBlades.methods + .fightXpGain() + .call(defaultCallOptions(rootState))); }, - async setFightXpGain({rootState}: {rootState: IState}, {xpGain}: {xpGain: number}) { - const {CryptoBlades} = rootState.contracts(); + async setFightXpGain( + { rootState }: { rootState: IState }, + { xpGain }: { xpGain: number } + ) { + const { CryptoBlades } = rootState.contracts(); if (!CryptoBlades) return; await CryptoBlades.methods.setFightXpGain(xpGain).send({ diff --git a/frontend/src/views/Combat.vue b/frontend/src/views/Combat.vue index 630a3a1a7..54fe83b2b 100644 --- a/frontend/src/views/Combat.vue +++ b/frontend/src/views/Combat.vue @@ -1,123 +1,187 @@