diff --git a/.gas-snapshot b/.gas-snapshot new file mode 100644 index 00000000..5367fcce --- /dev/null +++ b/.gas-snapshot @@ -0,0 +1,3 @@ +InlineEngineGasTest:test_consecutiveBattleGas() (gas: 18257376) +InlineEngineGasTest:test_identicalBattlesGas() (gas: 15177982) +InlineEngineGasTest:test_identicalBattlesWithEffectsGas() (gas: 19225213) \ No newline at end of file diff --git a/script/EngineAndPeriphery.s.sol b/script/EngineAndPeriphery.s.sol index e1057ef0..e1566cfe 100644 --- a/script/EngineAndPeriphery.s.sol +++ b/script/EngineAndPeriphery.s.sol @@ -52,7 +52,7 @@ contract EngineAndPeriphery is Script { TypeCalculator typeCalc = new TypeCalculator(); deployedContracts.push(DeployData({name: "TYPE CALCULATOR", contractAddress: address(typeCalc)})); - Engine engine = new Engine(); + Engine engine = new Engine(NUM_MONS, NUM_MOVES, TIMEOUT_DURATION); deployedContracts.push(DeployData({name: "ENGINE", contractAddress: address(engine)})); SignedCommitManager commitManager = new SignedCommitManager(engine); diff --git a/snapshots/EngineGasTest.json b/snapshots/EngineGasTest.json index ec09c611..822e1119 100644 --- a/snapshots/EngineGasTest.json +++ b/snapshots/EngineGasTest.json @@ -1,17 +1,21 @@ { - "B1_Execute": "960012", - "B1_Setup": "818789", - "B2_Execute": "717651", - "B2_Setup": "280825", - "Battle1_Execute": "502996", - "Battle1_Setup": "794107", - "Battle2_Execute": "395719", - "Battle2_Setup": "235771", - "FirstBattle": "3292183", + "B1_Execute": "941274", + "B1_Setup": "818936", + "B2_Execute": "698961", + "B2_Setup": "280870", + "Battle1_Execute": "505716", + "Battle1_Setup": "794246", + "Battle2_Execute": "398431", + "Battle2_Setup": "235910", + "External_Execute": "502798", + "External_Setup": "784961", + "FirstBattle": "3192359", + "Inline_Execute": "348349", + "Inline_Setup": "224527", "Intermediary stuff": "46798", - "SecondBattle": "3374477", - "Setup 1": "1675139", - "Setup 2": "297948", - "Setup 3": "340813", - "ThirdBattle": "2680640" + "SecondBattle": "3270181", + "Setup 1": "1675286", + "Setup 2": "298119", + "Setup 3": "340712", + "ThirdBattle": "2580771" } \ No newline at end of file diff --git a/snapshots/EngineTest.json b/snapshots/EngineTest.json deleted file mode 100644 index 86757e51..00000000 --- a/snapshots/EngineTest.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "FirstBattle": "6745022", - "Intermediary stuff": "47879", - "SecondBattle": "6826261", - "Setup 1": "1906149", - "Setup 2": "363778" -} \ No newline at end of file diff --git a/snapshots/InlineEngineGasTest.json b/snapshots/InlineEngineGasTest.json new file mode 100644 index 00000000..63925856 --- /dev/null +++ b/snapshots/InlineEngineGasTest.json @@ -0,0 +1,16 @@ +{ + "B1_Execute": "923868", + "B1_Setup": "757636", + "B2_Execute": "663118", + "B2_Setup": "267076", + "Battle1_Execute": "453108", + "Battle1_Setup": "732938", + "Battle2_Execute": "348301", + "Battle2_Setup": "223874", + "FirstBattle": "2907336", + "SecondBattle": "2950149", + "Setup 1": "1612801", + "Setup 2": "322003", + "Setup 3": "318468", + "ThirdBattle": "2295522" +} \ No newline at end of file diff --git a/snapshots/MatchmakerTest.json b/snapshots/MatchmakerTest.json index 9554ed1a..0afae43d 100644 --- a/snapshots/MatchmakerTest.json +++ b/snapshots/MatchmakerTest.json @@ -1,5 +1,5 @@ { - "Accept1": "312115", - "Accept2": "34013", - "Propose1": "197170" + "Accept1": "312198", + "Accept2": "34057", + "Propose1": "197214" } \ No newline at end of file diff --git a/src/DefaultValidator.sol b/src/DefaultValidator.sol index 6287bd62..ecaa0fc2 100644 --- a/src/DefaultValidator.sol +++ b/src/DefaultValidator.sol @@ -7,6 +7,7 @@ import "./moves/IMoveSet.sol"; import {IEngine} from "./IEngine.sol"; import {IValidator} from "./IValidator.sol"; +import {ValidatorLogic} from "./lib/ValidatorLogic.sol"; import {ICommitManager} from "./commit-manager/ICommitManager.sol"; import {IMonRegistry} from "./teams/IMonRegistry.sol"; @@ -86,22 +87,16 @@ contract DefaultValidator is IValidator { BattleContext memory ctx = ENGINE.getBattleContext(battleKey); uint256 activeMonIndex = (playerIndex == 0) ? ctx.p0ActiveMonIndex : ctx.p1ActiveMonIndex; - if (monToSwitchIndex >= MONS_PER_TEAM) { - return false; - } - bool isNewMonKnockedOut = + bool isTargetKnockedOut = ENGINE.getMonStateForBattle(battleKey, playerIndex, monToSwitchIndex, MonStateIndexName.IsKnockedOut) == 1; - if (isNewMonKnockedOut) { - return false; - } - // If it's not the zeroth turn, we cannot switch to the same mon - // (exception for zeroth turn because we have not initiated a swap yet, so index 0 is fine) - if (ctx.turnId != 0) { - if (monToSwitchIndex == activeMonIndex) { - return false; - } - } - return true; + + return ValidatorLogic.validateSwitch( + ctx.turnId, + activeMonIndex, + monToSwitchIndex, + isTargetKnockedOut, + MONS_PER_TEAM + ); } function validateSpecificMoveSelection( @@ -113,22 +108,21 @@ contract DefaultValidator is IValidator { BattleContext memory ctx = ENGINE.getBattleContext(battleKey); uint256 activeMonIndex = (playerIndex == 0) ? ctx.p0ActiveMonIndex : ctx.p1ActiveMonIndex; - // A move cannot be selected if its stamina costs more than the mon's current stamina IMoveSet moveSet = ENGINE.getMoveForMonForBattle(battleKey, playerIndex, activeMonIndex, moveIndex); - int256 monStaminaDelta = + int32 staminaDelta = ENGINE.getMonStateForBattle(battleKey, playerIndex, activeMonIndex, MonStateIndexName.Stamina); - uint256 monBaseStamina = + uint32 baseStamina = ENGINE.getMonValueForBattle(battleKey, playerIndex, activeMonIndex, MonStateIndexName.Stamina); - uint256 monCurrentStamina = uint256(int256(monBaseStamina) + monStaminaDelta); - if (moveSet.stamina(battleKey, playerIndex, activeMonIndex) > monCurrentStamina) { - return false; - } else { - // Then, we check the move itself to see if it enforces any other specific conditions - if (!moveSet.isValidTarget(battleKey, extraData)) { - return false; - } - } - return true; + + return ValidatorLogic.validateSpecificMoveSelection( + battleKey, + moveSet, + playerIndex, + activeMonIndex, + extraData, + baseStamina, + staminaDelta + ); } // Validates that you can't switch to the same mon, you have enough stamina, the move isn't disabled, etc. @@ -142,94 +136,30 @@ contract DefaultValidator is IValidator { uint256 activeMonIndex = (playerIndex == 0) ? vctx.p0ActiveMonIndex : vctx.p1ActiveMonIndex; bool isActiveMonKnockedOut = (playerIndex == 0) ? vctx.p0ActiveMonKnockedOut : vctx.p1ActiveMonKnockedOut; - // Enforce a switch IF: - // - if it is the zeroth turn - // - if the active mon is knocked out - { - bool isTurnZero = vctx.turnId == 0; - if (isTurnZero || isActiveMonKnockedOut) { - if (moveIndex != SWITCH_MOVE_INDEX) { - return false; - } - } - } + // Use library for basic validation + (bool requiresSwitch, bool isNoOp, bool isSwitch, bool isRegularMove, bool basicValid) = + ValidatorLogic.validatePlayerMoveBasics(moveIndex, vctx.turnId, isActiveMonKnockedOut, MOVES_PER_MON); - // Cannot go past the first 4 moves, or the switch move index or the no op - if (moveIndex != NO_OP_MOVE_INDEX && moveIndex != SWITCH_MOVE_INDEX) { - if (moveIndex >= MOVES_PER_MON) { - return false; - } + if (!basicValid) { + return false; } - // If it is no op move, it's valid - else if (moveIndex == NO_OP_MOVE_INDEX) { + + // No-op is always valid (if basic validation passed) + if (isNoOp) { return true; } - // If it is a switch move, then it's valid as long as the new mon isn't knocked out - // AND if the new mon isn't the same index as the existing mon - else if (moveIndex == SWITCH_MOVE_INDEX) { - // extraData contains the mon index to switch to as raw uint240 + + // Switch validation + if (isSwitch) { uint256 monToSwitchIndex = uint256(extraData); return _validateSwitchInternalWithContext(battleKey, playerIndex, monToSwitchIndex, vctx); } - // Otherwise, it's not a switch or a no-op, so it's a move - if (!_validateSpecificMoveSelectionWithContext(battleKey, moveIndex, playerIndex, extraData, activeMonIndex, vctx)) { - return false; - } - - return true; - } - - // Internal version that accepts pre-fetched context to avoid redundant calls - function _validateSwitchInternal( - bytes32 battleKey, - uint256 playerIndex, - uint256 monToSwitchIndex, - BattleContext memory ctx - ) internal view returns (bool) { - uint256 activeMonIndex = (playerIndex == 0) ? ctx.p0ActiveMonIndex : ctx.p1ActiveMonIndex; - - if (monToSwitchIndex >= MONS_PER_TEAM) { - return false; - } - bool isNewMonKnockedOut = - ENGINE.getMonStateForBattle(battleKey, playerIndex, monToSwitchIndex, MonStateIndexName.IsKnockedOut) == 1; - if (isNewMonKnockedOut) { - return false; - } - // If it's not the zeroth turn, we cannot switch to the same mon - // (exception for zeroth turn because we have not initiated a swap yet, so index 0 is fine) - if (ctx.turnId != 0) { - if (monToSwitchIndex == activeMonIndex) { - return false; - } + // Regular move validation + if (isRegularMove) { + return _validateSpecificMoveSelectionWithContext(battleKey, moveIndex, playerIndex, extraData, activeMonIndex, vctx); } - return true; - } - // Internal version that accepts pre-fetched activeMonIndex to avoid redundant calls - function _validateSpecificMoveSelectionInternal( - bytes32 battleKey, - uint256 moveIndex, - uint256 playerIndex, - uint240 extraData, - uint256 activeMonIndex - ) internal view returns (bool) { - // A move cannot be selected if its stamina costs more than the mon's current stamina - IMoveSet moveSet = ENGINE.getMoveForMonForBattle(battleKey, playerIndex, activeMonIndex, moveIndex); - int256 monStaminaDelta = - ENGINE.getMonStateForBattle(battleKey, playerIndex, activeMonIndex, MonStateIndexName.Stamina); - uint256 monBaseStamina = - ENGINE.getMonValueForBattle(battleKey, playerIndex, activeMonIndex, MonStateIndexName.Stamina); - uint256 monCurrentStamina = uint256(int256(monBaseStamina) + monStaminaDelta); - if (moveSet.stamina(battleKey, playerIndex, activeMonIndex) > monCurrentStamina) { - return false; - } else { - // Then, we check the move itself to see if it enforces any other specific conditions - if (!moveSet.isValidTarget(battleKey, extraData)) { - return false; - } - } return true; } @@ -242,22 +172,17 @@ contract DefaultValidator is IValidator { ) internal view returns (bool) { uint256 activeMonIndex = (playerIndex == 0) ? vctx.p0ActiveMonIndex : vctx.p1ActiveMonIndex; - if (monToSwitchIndex >= MONS_PER_TEAM) { - return false; - } // Still need external call to check if switch target is KO'd (not in context) - bool isNewMonKnockedOut = + bool isTargetKnockedOut = ENGINE.getMonStateForBattle(battleKey, playerIndex, monToSwitchIndex, MonStateIndexName.IsKnockedOut) == 1; - if (isNewMonKnockedOut) { - return false; - } - // If it's not the zeroth turn, we cannot switch to the same mon - if (vctx.turnId != 0) { - if (monToSwitchIndex == activeMonIndex) { - return false; - } - } - return true; + + return ValidatorLogic.validateSwitch( + vctx.turnId, + activeMonIndex, + monToSwitchIndex, + isTargetKnockedOut, + MONS_PER_TEAM + ); } // Internal version using ValidationContext for stamina check @@ -270,20 +195,21 @@ contract DefaultValidator is IValidator { ValidationContext memory vctx ) internal view returns (bool) { // Use pre-fetched stamina values from context - uint256 monBaseStamina = (playerIndex == 0) ? vctx.p0ActiveMonBaseStamina : vctx.p1ActiveMonBaseStamina; - int256 monStaminaDelta = (playerIndex == 0) ? vctx.p0ActiveMonStaminaDelta : vctx.p1ActiveMonStaminaDelta; - uint256 monCurrentStamina = uint256(int256(monBaseStamina) + monStaminaDelta); + uint32 baseStamina = (playerIndex == 0) ? vctx.p0ActiveMonBaseStamina : vctx.p1ActiveMonBaseStamina; + int32 staminaDelta = (playerIndex == 0) ? vctx.p0ActiveMonStaminaDelta : vctx.p1ActiveMonStaminaDelta; // Still need external call to get the move (can't batch all moves) IMoveSet moveSet = ENGINE.getMoveForMonForBattle(battleKey, playerIndex, activeMonIndex, moveIndex); - if (moveSet.stamina(battleKey, playerIndex, activeMonIndex) > monCurrentStamina) { - return false; - } else { - if (!moveSet.isValidTarget(battleKey, extraData)) { - return false; - } - } - return true; + + return ValidatorLogic.validateSpecificMoveSelection( + battleKey, + moveSet, + playerIndex, + activeMonIndex, + extraData, + baseStamina, + staminaDelta + ); } /* diff --git a/src/Engine.sol b/src/Engine.sol index 6fc03707..ca3adc54 100644 --- a/src/Engine.sol +++ b/src/Engine.sol @@ -8,13 +8,23 @@ import "./Structs.sol"; import "./moves/IMoveSet.sol"; import {IEngine} from "./IEngine.sol"; +import {ICommitManager} from "./commit-manager/ICommitManager.sol"; import {MappingAllocator} from "./lib/MappingAllocator.sol"; +import {ValidatorLogic} from "./lib/ValidatorLogic.sol"; import {IMatchmaker} from "./matchmaker/IMatchmaker.sol"; contract Engine is IEngine, MappingAllocator { + // Default validator config (immutable, for inline validation when validator is address(0)) + uint256 public immutable DEFAULT_MONS_PER_TEAM; + uint256 public immutable DEFAULT_MOVES_PER_MON; + uint256 public immutable DEFAULT_TIMEOUT_DURATION; + uint256 public constant PREV_TURN_MULTIPLIER = 2; bytes32 public transient battleKeyForWrite; // intended to be used during call stack by other contracts bytes32 private transient storageKeyForWrite; // cached storage key to avoid repeated lookups + // Bitmap tracking which effect lists were modified (for caching effect counts) + // Bit 0: global effects, Bits 1-8: P0 mons 0-7, Bits 9-16: P1 mons 0-7 + uint256 private transient effectsDirtyBitmap; mapping(bytes32 => uint256) public pairHashNonces; // imposes a global ordering across all matches mapping(address player => mapping(address maker => bool)) public isMatchmakerFor; // tracks approvals for matchmakers @@ -106,16 +116,31 @@ contract Engine is IEngine, MappingAllocator { uint256 step ); event BattleComplete(bytes32 indexed battleKey, address winner); - event EngineEvent( - bytes32 indexed battleKey, bytes32 eventType, bytes eventData, address source, uint256 step - ); + event EngineEvent(bytes32 indexed battleKey, bytes32 eventType, bytes eventData, address source, uint256 step); + + /// @notice Constructor to set default validator config for inline validation + /// @dev When a battle's validator is address(0), Engine uses inline validation logic with these params + /// @param _DEFAULT_MONS_PER_TEAM Default mons per team for inline validation + /// @param _DEFAULT_MOVES_PER_MON Default moves per mon for inline validation + /// @param _DEFAULT_TIMEOUT_DURATION Default timeout duration for inline validation + constructor(uint256 _DEFAULT_MONS_PER_TEAM, uint256 _DEFAULT_MOVES_PER_MON, uint256 _DEFAULT_TIMEOUT_DURATION) { + DEFAULT_MONS_PER_TEAM = _DEFAULT_MONS_PER_TEAM; + DEFAULT_MOVES_PER_MON = _DEFAULT_MOVES_PER_MON; + DEFAULT_TIMEOUT_DURATION = _DEFAULT_TIMEOUT_DURATION; + } function updateMatchmakers(address[] memory makersToAdd, address[] memory makersToRemove) external { - for (uint256 i; i < makersToAdd.length; ++i) { + for (uint256 i; i < makersToAdd.length;) { isMatchmakerFor[msg.sender][makersToAdd[i]] = true; + unchecked { + ++i; + } } - for (uint256 i; i < makersToRemove.length; ++i) { + for (uint256 i; i < makersToRemove.length;) { isMatchmakerFor[msg.sender][makersToRemove[i]] = false; + unchecked { + ++i; + } } } @@ -146,7 +171,7 @@ contract Engine is IEngine, MappingAllocator { // Clear previous battle's mon states by setting non-zero values to sentinel // MonState packs into a single 256-bit slot (7 x int32 + 2 x bool = 240 bits) // We use assembly to read/write the entire slot in one operation - for (uint256 j = 0; j < prevP0Size; j++) { + for (uint256 j = 0; j < prevP0Size;) { MonState storage monState = config.p0States[j]; assembly { let slot := monState.slot @@ -154,8 +179,11 @@ contract Engine is IEngine, MappingAllocator { sstore(slot, PACKED_CLEARED_MON_STATE) } } + unchecked { + ++j; + } } - for (uint256 j = 0; j < prevP1Size; j++) { + for (uint256 j = 0; j < prevP1Size;) { MonState storage monState = config.p1States[j]; assembly { let slot := monState.slot @@ -163,13 +191,16 @@ contract Engine is IEngine, MappingAllocator { sstore(slot, PACKED_CLEARED_MON_STATE) } } + unchecked { + ++j; + } } // Store the battle config (update fields individually to preserve effects mapping slots) - if (config.validator != battle.validator) { + if (address(config.validator) != address(battle.validator)) { config.validator = battle.validator; } - if (config.rngOracle != battle.rngOracle) { + if (address(config.rngOracle) != address(battle.rngOracle)) { config.rngOracle = battle.rngOracle; } if (config.moveManager != battle.moveManager) { @@ -192,10 +223,8 @@ contract Engine is IEngine, MappingAllocator { }); // Set the team for p0 and p1 in the reusable config storage - (Mon[] memory p0Team, Mon[] memory p1Team) = battle.teamRegistry.getTeams( - battle.p0, battle.p0TeamIndex, - battle.p1, battle.p1TeamIndex - ); + (Mon[] memory p0Team, Mon[] memory p1Team) = + battle.teamRegistry.getTeams(battle.p0, battle.p0TeamIndex, battle.p1, battle.p1TeamIndex); // Store actual team sizes (packed: lower 4 bits = p0, upper 4 bits = p1) uint256 p0Len = p0Team.length; @@ -203,11 +232,17 @@ contract Engine is IEngine, MappingAllocator { config.teamSizes = uint8(p0Len) | (uint8(p1Len) << 4); // Store teams in mappings - for (uint256 j = 0; j < p0Len; j++) { + for (uint256 j = 0; j < p0Len;) { config.p0Team[j] = p0Team[j]; + unchecked { + ++j; + } } - for (uint256 j = 0; j < p1Len; j++) { + for (uint256 j = 0; j < p1Len;) { config.p1Team[j] = p1Team[j]; + unchecked { + ++j; + } } // Set the global effects and data to start the game if any @@ -215,10 +250,13 @@ contract Engine is IEngine, MappingAllocator { (IEffect[] memory effects, bytes32[] memory data) = battle.ruleset.getInitialGlobalEffects(); uint256 numEffects = effects.length; if (numEffects > 0) { - for (uint i = 0; i < numEffects; ++i) { + for (uint256 i = 0; i < numEffects;) { config.globalEffects[i].effect = effects[i]; config.globalEffects[i].stepsBitmap = effects[i].getStepsBitmap(); config.globalEffects[i].data = data[i]; + unchecked { + ++i; + } } config.globalEffectsLength = uint8(effects.length); } @@ -229,12 +267,14 @@ contract Engine is IEngine, MappingAllocator { // Set the engine hooks to start the game if any uint256 numHooks = battle.engineHooks.length; if (numHooks > 0) { - for (uint i; i < numHooks; ++i) { + for (uint256 i; i < numHooks;) { config.engineHooks[i] = battle.engineHooks[i]; + unchecked { + ++i; + } } config.engineHooksLength = uint8(numHooks); - } - else { + } else { config.engineHooksLength = 0; } @@ -246,15 +286,21 @@ contract Engine is IEngine, MappingAllocator { teams[0] = p0Team; teams[1] = p1Team; - // Validate the battle config - if (!battle.validator - .validateGameStart(battle.p0, battle.p1, teams, battle.teamRegistry, battle.p0TeamIndex, battle.p1TeamIndex)) - { - revert InvalidBattleConfig(); + // Validate the battle config (skip if using inline validation) + if (address(battle.validator) != address(0)) { + if (!battle.validator + .validateGameStart( + battle.p0, battle.p1, teams, battle.teamRegistry, battle.p0TeamIndex, battle.p1TeamIndex + )) { + revert InvalidBattleConfig(); + } } - for (uint256 i = 0; i < battle.engineHooks.length; ++i) { + for (uint256 i = 0; i < battle.engineHooks.length;) { battle.engineHooks[i].onBattleStart(battleKey); + unchecked { + ++i; + } } emit BattleStart(battleKey, battle.p0, battle.p1); @@ -269,7 +315,10 @@ contract Engine is IEngine, MappingAllocator { BattleConfig storage config = battleConfig[storageKey]; // Check that at least one move has been set (isRealTurn is stored in bit 7 of packedMoveIndex) - if ((config.p0Move.packedMoveIndex & IS_REAL_TURN_BIT) == 0 && (config.p1Move.packedMoveIndex & IS_REAL_TURN_BIT) == 0) { + if ( + (config.p0Move.packedMoveIndex & IS_REAL_TURN_BIT) == 0 + && (config.p1Move.packedMoveIndex & IS_REAL_TURN_BIT) == 0 + ) { revert MovesNotSet(); } @@ -338,8 +387,11 @@ contract Engine is IEngine, MappingAllocator { battleKeyForWrite = battleKey; uint256 numHooks = config.engineHooksLength; - for (uint256 i = 0; i < numHooks; ++i) { + for (uint256 i = 0; i < numHooks;) { config.engineHooks[i].onRoundStart(battleKey); + unchecked { + ++i; + } } // If only a single player has a move to submit, then we don't trigger any effects @@ -379,7 +431,6 @@ contract Engine is IEngine, MappingAllocator { - Set player switch for turn flag */ else { - // Update the temporary RNG to the newest value uint256 rng = config.rngOracle.getRNG(config.p0Salt, config.p1Salt); tempRNG = rng; @@ -390,7 +441,15 @@ contract Engine is IEngine, MappingAllocator { // Run beginning of round effects playerSwitchForTurnFlag = _handleEffects( - battleKey, config, battle, rng, 2, 2, EffectStep.RoundStart, EffectRunCondition.SkipIfGameOver, playerSwitchForTurnFlag + battleKey, + config, + battle, + rng, + 2, + 2, + EffectStep.RoundStart, + EffectRunCondition.SkipIfGameOver, + playerSwitchForTurnFlag ); playerSwitchForTurnFlag = _handleEffects( battleKey, @@ -416,7 +475,8 @@ contract Engine is IEngine, MappingAllocator { ); // Run priority player's move (NOTE: moves won't run if either mon is KOed) - playerSwitchForTurnFlag = _handleMove(battleKey, config, battle, priorityPlayerIndex, playerSwitchForTurnFlag); + playerSwitchForTurnFlag = + _handleMove(battleKey, config, battle, priorityPlayerIndex, playerSwitchForTurnFlag); // If priority mons is not KO'ed, then run the priority player's mon's afterMove hook(s) playerSwitchForTurnFlag = _handleEffects( @@ -490,7 +550,15 @@ contract Engine is IEngine, MappingAllocator { // Always run global effects at the end of the round playerSwitchForTurnFlag = _handleEffects( - battleKey, config, battle, rng, 2, 2, EffectStep.RoundEnd, EffectRunCondition.SkipIfGameOver, playerSwitchForTurnFlag + battleKey, + config, + battle, + rng, + 2, + 2, + EffectStep.RoundEnd, + EffectRunCondition.SkipIfGameOver, + playerSwitchForTurnFlag ); // If priority mon is not KOed, run roundEnd effects for the priority mon @@ -521,8 +589,11 @@ contract Engine is IEngine, MappingAllocator { } // Run the round end hooks - for (uint256 i = 0; i < numHooks; ++i) { + for (uint256 i = 0; i < numHooks;) { config.engineHooks[i].onRoundEnd(battleKey); + unchecked { + ++i; + } } // If a winner has been set, handle the game over @@ -558,14 +629,24 @@ contract Engine is IEngine, MappingAllocator { if (data.winnerIndex != 2) { revert GameAlreadyOver(); } - for (uint256 i; i < 2; ++i) { - address potentialLoser = config.validator.validateTimeout(battleKey, i); + for (uint256 i; i < 2;) { + address potentialLoser; + if (address(config.validator) != address(0)) { + potentialLoser = config.validator.validateTimeout(battleKey, i); + } + // Use inline timeout validation when validator is address(0) + else { + potentialLoser = _validateTimeoutInline(battleKey, data, config, i); + } if (potentialLoser != address(0)) { address winner = potentialLoser == data.p0 ? data.p1 : data.p0; data.winnerIndex = (winner == data.p0) ? 0 : 1; _handleGameOver(battleKey, winner); return; } + unchecked { + ++i; + } } // Allow forcible end of battle after max duration if (block.timestamp - config.startTimestamp > MAX_BATTLE_DURATION) { @@ -574,6 +655,79 @@ contract Engine is IEngine, MappingAllocator { } } + /// @dev Inline timeout validation logic (mirrors DefaultValidator.validateTimeout) + function _validateTimeoutInline( + bytes32 battleKey, + BattleData storage data, + BattleConfig storage config, + uint256 playerIndexToCheck + ) private view returns (address loser) { + uint256 otherPlayerIndex = 1 - playerIndexToCheck; + uint64 turnId = data.turnId; + uint256 playerSwitchForTurnFlag = data.playerSwitchForTurnFlag; + + ICommitManager commitManager = ICommitManager(config.moveManager); + address[2] memory players = [data.p0, data.p1]; + + uint256 lastTurnTimestamp; + // Turn 0: use battle start timestamp + // Otherwise: use lastExecuteTimestamp from engine + if (turnId == 0) { + lastTurnTimestamp = config.startTimestamp; + } else { + lastTurnTimestamp = config.lastExecuteTimestamp; + } + + // It's a single player turn, and it's our turn: + if (playerSwitchForTurnFlag == playerIndexToCheck) { + if (block.timestamp >= lastTurnTimestamp + PREV_TURN_MULTIPLIER * DEFAULT_TIMEOUT_DURATION) { + return players[playerIndexToCheck]; + } + } + // It's a two player turn: + else if (playerSwitchForTurnFlag == 2) { + // We are committing + revealing: + if (turnId % 2 == playerIndexToCheck) { + (bytes32 playerMoveHash, uint256 playerTurnId) = + commitManager.getCommitment(battleKey, players[playerIndexToCheck]); + // If we have already committed: + if (playerTurnId == turnId && playerMoveHash != bytes32(0)) { + // Check if other player has already revealed + uint256 numMovesOtherPlayerRevealed = + commitManager.getMoveCountForBattleState(battleKey, players[otherPlayerIndex]); + uint256 otherPlayerTimestamp = + commitManager.getLastMoveTimestampForPlayer(battleKey, players[otherPlayerIndex]); + // If so, then check for timeout + if (numMovesOtherPlayerRevealed > turnId) { + if (block.timestamp >= otherPlayerTimestamp + DEFAULT_TIMEOUT_DURATION) { + return players[playerIndexToCheck]; + } + } + } + // If we have not committed yet: + else { + if (block.timestamp >= lastTurnTimestamp + PREV_TURN_MULTIPLIER * DEFAULT_TIMEOUT_DURATION) { + return players[playerIndexToCheck]; + } + } + } + // We are revealing: + else { + (bytes32 otherPlayerMoveHash, uint256 otherPlayerTurnId) = + commitManager.getCommitment(battleKey, players[otherPlayerIndex]); + // If other player has already committed: + if (otherPlayerTurnId == turnId && otherPlayerMoveHash != bytes32(0)) { + uint256 otherPlayerTimestamp = + commitManager.getLastMoveTimestampForPlayer(battleKey, players[otherPlayerIndex]); + if (block.timestamp >= otherPlayerTimestamp + DEFAULT_TIMEOUT_DURATION) { + return players[playerIndexToCheck]; + } + } + } + } + return address(0); + } + function _handleGameOver(bytes32 battleKey, address winner) internal { bytes32 storageKey = storageKeyForWrite; BattleConfig storage config = battleConfig[storageKey]; @@ -582,8 +736,11 @@ contract Engine is IEngine, MappingAllocator { revert GameStartsAndEndsSameBlock(); } - for (uint256 i = 0; i < config.engineHooksLength; ++i) { + for (uint256 i = 0; i < config.engineHooksLength;) { config.engineHooks[i].onBattleEnd(battleKey); + unchecked { + ++i; + } } // Free the key used for battle configs so other battles can use it @@ -604,19 +761,28 @@ contract Engine is IEngine, MappingAllocator { BattleConfig storage config = battleConfig[storageKeyForWrite]; MonState storage monState = _getMonState(config, playerIndex, monIndex); if (stateVarIndex == MonStateIndexName.Hp) { - monState.hpDelta = (monState.hpDelta == CLEARED_MON_STATE_SENTINEL) ? valueToAdd : monState.hpDelta + valueToAdd; + monState.hpDelta = + (monState.hpDelta == CLEARED_MON_STATE_SENTINEL) ? valueToAdd : monState.hpDelta + valueToAdd; } else if (stateVarIndex == MonStateIndexName.Stamina) { - monState.staminaDelta = (monState.staminaDelta == CLEARED_MON_STATE_SENTINEL) ? valueToAdd : monState.staminaDelta + valueToAdd; + monState.staminaDelta = + (monState.staminaDelta == CLEARED_MON_STATE_SENTINEL) ? valueToAdd : monState.staminaDelta + valueToAdd; } else if (stateVarIndex == MonStateIndexName.Speed) { - monState.speedDelta = (monState.speedDelta == CLEARED_MON_STATE_SENTINEL) ? valueToAdd : monState.speedDelta + valueToAdd; + monState.speedDelta = + (monState.speedDelta == CLEARED_MON_STATE_SENTINEL) ? valueToAdd : monState.speedDelta + valueToAdd; } else if (stateVarIndex == MonStateIndexName.Attack) { - monState.attackDelta = (monState.attackDelta == CLEARED_MON_STATE_SENTINEL) ? valueToAdd : monState.attackDelta + valueToAdd; + monState.attackDelta = + (monState.attackDelta == CLEARED_MON_STATE_SENTINEL) ? valueToAdd : monState.attackDelta + valueToAdd; } else if (stateVarIndex == MonStateIndexName.Defense) { - monState.defenceDelta = (monState.defenceDelta == CLEARED_MON_STATE_SENTINEL) ? valueToAdd : monState.defenceDelta + valueToAdd; + monState.defenceDelta = + (monState.defenceDelta == CLEARED_MON_STATE_SENTINEL) ? valueToAdd : monState.defenceDelta + valueToAdd; } else if (stateVarIndex == MonStateIndexName.SpecialAttack) { - monState.specialAttackDelta = (monState.specialAttackDelta == CLEARED_MON_STATE_SENTINEL) ? valueToAdd : monState.specialAttackDelta + valueToAdd; + monState.specialAttackDelta = (monState.specialAttackDelta == CLEARED_MON_STATE_SENTINEL) + ? valueToAdd + : monState.specialAttackDelta + valueToAdd; } else if (stateVarIndex == MonStateIndexName.SpecialDefense) { - monState.specialDefenceDelta = (monState.specialDefenceDelta == CLEARED_MON_STATE_SENTINEL) ? valueToAdd : monState.specialDefenceDelta + valueToAdd; + monState.specialDefenceDelta = (monState.specialDefenceDelta == CLEARED_MON_STATE_SENTINEL) + ? valueToAdd + : monState.specialDefenceDelta + valueToAdd; } else if (stateVarIndex == MonStateIndexName.IsKnockedOut) { bool newKOState = (valueToAdd % 2) == 1; bool wasKOed = monState.isKnockedOut; @@ -658,7 +824,7 @@ contract Engine is IEngine, MappingAllocator { if (battleKey == bytes32(0)) { revert NoWriteAllowed(); } - if (effect.shouldApply(extraData, targetIndex, monIndex)) { + if (effect.shouldApply(battleKey, extraData, targetIndex, monIndex)) { bytes32 extraDataToUse = extraData; bool removeAfterRun = false; @@ -678,8 +844,12 @@ contract Engine is IEngine, MappingAllocator { // Check if we have to run an onApply state update (use bitmap instead of external call) if ((stepsBitmap & (1 << uint8(EffectStep.OnApply))) != 0) { + // Get active mon indices for both players + BattleData storage battle = battleData[battleKey]; + uint256 p0ActiveMonIndex = _unpackActiveMonIndex(battle.activeMonIndex, 0); + uint256 p1ActiveMonIndex = _unpackActiveMonIndex(battle.activeMonIndex, 1); // If so, we run the effect first, and get updated extraData if necessary - (extraDataToUse, removeAfterRun) = effect.onApply(tempRNG, extraData, targetIndex, monIndex); + (extraDataToUse, removeAfterRun) = effect.onApply(battleKey, tempRNG, extraData, targetIndex, monIndex, p0ActiveMonIndex, p1ActiveMonIndex); } if (!removeAfterRun) { // Add to the appropriate effects mapping based on targetIndex @@ -693,6 +863,8 @@ contract Engine is IEngine, MappingAllocator { effectSlot.stepsBitmap = stepsBitmap; effectSlot.data = extraDataToUse; config.globalEffectsLength = uint8(effectIndex + 1); + // Set dirty bit 0 for global effects + effectsDirtyBitmap |= 1; } else if (targetIndex == 0) { // Player effects use per-mon indexing: slot = MAX_EFFECTS_PER_MON * monIndex + count[monIndex] uint256 monEffectCount = _getMonEffectCount(config.packedP0EffectsCount, monIndex); @@ -701,7 +873,10 @@ contract Engine is IEngine, MappingAllocator { effectSlot.effect = effect; effectSlot.stepsBitmap = stepsBitmap; effectSlot.data = extraDataToUse; - config.packedP0EffectsCount = _setMonEffectCount(config.packedP0EffectsCount, monIndex, monEffectCount + 1); + config.packedP0EffectsCount = + _setMonEffectCount(config.packedP0EffectsCount, monIndex, monEffectCount + 1); + // Set dirty bit (1 + monIndex) for P0 effects + effectsDirtyBitmap |= (1 << (1 + monIndex)); } else { uint256 monEffectCount = _getMonEffectCount(config.packedP1EffectsCount, monIndex); uint256 slotIndex = _getEffectSlotIndex(monIndex, monEffectCount); @@ -709,15 +884,16 @@ contract Engine is IEngine, MappingAllocator { effectSlot.effect = effect; effectSlot.stepsBitmap = stepsBitmap; effectSlot.data = extraDataToUse; - config.packedP1EffectsCount = _setMonEffectCount(config.packedP1EffectsCount, monIndex, monEffectCount + 1); + config.packedP1EffectsCount = + _setMonEffectCount(config.packedP1EffectsCount, monIndex, monEffectCount + 1); + // Set dirty bit (9 + monIndex) for P1 effects + effectsDirtyBitmap |= (1 << (9 + monIndex)); } } } } - function editEffect(uint256 targetIndex, uint256 monIndex, uint256 effectIndex, bytes32 newExtraData) - external - { + function editEffect(uint256 targetIndex, uint256 monIndex, uint256 effectIndex, bytes32 newExtraData) external { bytes32 battleKey = battleKeyForWrite; if (battleKey == bytes32(0)) { revert NoWriteAllowed(); @@ -781,7 +957,11 @@ contract Engine is IEngine, MappingAllocator { // Use stored bitmap instead of external call to shouldRunAtStep() if ((stepsBitmap & (1 << uint8(EffectStep.OnRemove))) != 0) { - effect.onRemove(data, 2, monIndex); + // Get active mon indices for both players + BattleData storage battle = battleData[battleKey]; + uint256 p0ActiveMonIndex = _unpackActiveMonIndex(battle.activeMonIndex, 0); + uint256 p1ActiveMonIndex = _unpackActiveMonIndex(battle.activeMonIndex, 1); + effect.onRemove(battleKey, data, 2, monIndex, p0ActiveMonIndex, p1ActiveMonIndex); } // Tombstone the effect (indices are stable, no need to re-find) @@ -811,13 +991,19 @@ contract Engine is IEngine, MappingAllocator { // Use stored bitmap instead of external call to shouldRunAtStep() if ((stepsBitmap & (1 << uint8(EffectStep.OnRemove))) != 0) { - effect.onRemove(data, targetIndex, monIndex); + // Get active mon indices for both players + BattleData storage battle = battleData[battleKey]; + uint256 p0ActiveMonIndex = _unpackActiveMonIndex(battle.activeMonIndex, 0); + uint256 p1ActiveMonIndex = _unpackActiveMonIndex(battle.activeMonIndex, 1); + effect.onRemove(battleKey, data, targetIndex, monIndex, p0ActiveMonIndex, p1ActiveMonIndex); } // Tombstone the effect (indices are stable, no need to re-find) effectToRemove.effect = IEffect(TOMBSTONE_ADDRESS); - emit EffectRemove(battleKey, targetIndex, monIndex, address(effect), _getUpstreamCallerAndResetValue(), currentStep); + emit EffectRemove( + battleKey, targetIndex, monIndex, address(effect), _getUpstreamCallerAndResetValue(), currentStep + ); } function setGlobalKV(bytes32 key, uint192 value) external { @@ -864,8 +1050,19 @@ contract Engine is IEngine, MappingAllocator { BattleData storage battle = battleData[battleKey]; // Use the validator to check if the switch is valid - if (config.validator.validateSwitch(battleKey, playerIndex, monToSwitchIndex)) - { + bool isValid; + if (address(config.validator) == address(0)) { + // Use inline validation (no external call) + uint256 activeMonIndex = _unpackActiveMonIndex(battle.activeMonIndex, playerIndex); + bool isTargetKnockedOut = _getMonState(config, playerIndex, monToSwitchIndex).isKnockedOut; + isValid = ValidatorLogic.validateSwitch( + battle.turnId, activeMonIndex, monToSwitchIndex, isTargetKnockedOut, DEFAULT_MONS_PER_TEAM + ); + } else { + // Use external validator + isValid = config.validator.validateSwitch(battleKey, playerIndex, monToSwitchIndex); + } + if (isValid) { // Only call the internal switch function if the switch is valid _handleSwitch(battleKey, playerIndex, monToSwitchIndex, msg.sender); @@ -933,11 +1130,10 @@ contract Engine is IEngine, MappingAllocator { battleKey = keccak256(abi.encode(pairHash, pairHashNonce)); } - function _checkForGameOverOrKO( - BattleConfig storage config, - BattleData storage battle, - uint256 priorityPlayerIndex - ) internal returns (uint256 playerSwitchForTurnFlag, bool isGameOver) { + function _checkForGameOverOrKO(BattleConfig storage config, BattleData storage battle, uint256 priorityPlayerIndex) + internal + returns (uint256 playerSwitchForTurnFlag, bool isGameOver) + { uint256 otherPlayerIndex = (priorityPlayerIndex + 1) % 2; uint8 existingWinnerIndex = battle.winnerIndex; @@ -1075,27 +1271,40 @@ contract Engine is IEngine, MappingAllocator { } // Execute the move and then set updated state, active mons, and effects/data else { - // Call validateSpecificMoveSelection again from the validator to ensure that it is still valid to execute + IMoveSet moveSet = _getTeamMon(config, playerIndex, activeMonIndex).moves[moveIndex]; + + // Call validateSpecificMoveSelection again to ensure it is still valid to execute // If not, then we just return early // Handles cases where e.g. some condition outside of the player's control leads to an invalid move - if (!config.validator.validateSpecificMoveSelection(battleKey, moveIndex, playerIndex, move.extraData)) - { + bool isValid; + if (address(config.validator) == address(0)) { + // Use inline validation (no external call) + uint32 baseStamina = _getTeamMon(config, playerIndex, activeMonIndex).stats.stamina; + int32 staminaDelta = currentMonState.staminaDelta; + isValid = ValidatorLogic.validateSpecificMoveSelection( + battleKey, moveSet, playerIndex, activeMonIndex, move.extraData, baseStamina, staminaDelta + ); + } else { + // Use external validator + isValid = + config.validator.validateSpecificMoveSelection(battleKey, moveIndex, playerIndex, move.extraData); + } + if (!isValid) { return playerSwitchForTurnFlag; } - IMoveSet moveSet = _getTeamMon(config, playerIndex, activeMonIndex).moves[moveIndex]; - // Update the mon state directly to account for the stamina cost of the move staminaCost = int32(moveSet.stamina(battleKey, playerIndex, activeMonIndex)); - MonState storage monState = _getMonState(config, playerIndex, activeMonIndex); - monState.staminaDelta = - (monState.staminaDelta == CLEARED_MON_STATE_SENTINEL) ? -staminaCost : monState.staminaDelta - staminaCost; + currentMonState.staminaDelta = (currentMonState.staminaDelta == CLEARED_MON_STATE_SENTINEL) + ? -staminaCost + : currentMonState.staminaDelta - staminaCost; // Emit event and then run the move emit MonMove(battleKey, playerIndex, activeMonIndex, moveIndex, move.extraData, staminaCost); - // Run the move (no longer checking for a return value) - moveSet.move(battleKey, playerIndex, move.extraData, tempRNG); + // Run the move with both active mon indices to avoid external lookups + uint256 defenderMonIndex = _unpackActiveMonIndex(battle.activeMonIndex, 1 - playerIndex); + moveSet.move(battleKey, playerIndex, activeMonIndex, defenderMonIndex, move.extraData, tempRNG); } // Set Game Over if true, and calculate and return switch for turn flag @@ -1118,6 +1327,10 @@ contract Engine is IEngine, MappingAllocator { BattleData storage battle = battleData[battleKey]; BattleConfig storage config = battleConfig[storageKeyForWrite]; + // Get active mon indices for both players (passed to all effect hooks) + uint256 p0ActiveMonIndex = _unpackActiveMonIndex(battle.activeMonIndex, 0); + uint256 p1ActiveMonIndex = _unpackActiveMonIndex(battle.activeMonIndex, 1); + uint256 monIndex; // Determine the mon index for the target if (effectIndex == 2) { @@ -1141,21 +1354,29 @@ contract Engine is IEngine, MappingAllocator { baseSlot = _getEffectSlotIndex(monIndex, 0); } - // Use a loop index that reads current length each iteration (allows processing newly added effects) - uint256 i = 0; - while (true) { - // Get current length (may grow if effects add new effects) - uint256 effectsCount; - if (effectIndex == 2) { - effectsCount = config.globalEffectsLength; - } else if (effectIndex == 0) { - effectsCount = _getMonEffectCount(config.packedP0EffectsCount, monIndex); - } else { - effectsCount = _getMonEffectCount(config.packedP1EffectsCount, monIndex); - } + // Compute the dirty bit for this effect/mon combination + // Bit 0: global, Bits 1-8: P0 mons 0-7, Bits 9-16: P1 mons 0-7 + uint256 dirtyBit; + if (effectIndex == 2) { + dirtyBit = 1; + } else if (effectIndex == 0) { + dirtyBit = 1 << (1 + monIndex); + } else { + dirtyBit = 1 << (9 + monIndex); + } - if (i >= effectsCount) break; + // Cache the initial effect count (only re-read if dirty bit is set) + uint256 effectsCount; + if (effectIndex == 2) { + effectsCount = config.globalEffectsLength; + } else if (effectIndex == 0) { + effectsCount = _getMonEffectCount(config.packedP0EffectsCount, monIndex); + } else { + effectsCount = _getMonEffectCount(config.packedP1EffectsCount, monIndex); + } + uint256 i = 0; + while (i < effectsCount) { // Read effect directly from storage EffectInstance storage eff; uint256 slotIndex; @@ -1173,12 +1394,36 @@ contract Engine is IEngine, MappingAllocator { // Skip tombstoned effects if (address(eff.effect) != TOMBSTONE_ADDRESS) { _runSingleEffect( - config, rng, effectIndex, playerIndex, monIndex, round, extraEffectsData, - eff.effect, eff.stepsBitmap, eff.data, uint96(slotIndex) + config, + rng, + effectIndex, + playerIndex, + monIndex, + round, + extraEffectsData, + eff.effect, + eff.stepsBitmap, + eff.data, + uint96(slotIndex), + p0ActiveMonIndex, + p1ActiveMonIndex ); + + // Check if this effect added new effects (dirty bit set) + // Only re-read count if dirty, then clear the bit + if (effectsDirtyBitmap & dirtyBit != 0) { + if (effectIndex == 2) { + effectsCount = config.globalEffectsLength; + } else if (effectIndex == 0) { + effectsCount = _getMonEffectCount(config.packedP0EffectsCount, monIndex); + } else { + effectsCount = _getMonEffectCount(config.packedP1EffectsCount, monIndex); + } + effectsDirtyBitmap &= ~dirtyBit; + } } - ++i; + unchecked { ++i; } } } @@ -1193,7 +1438,9 @@ contract Engine is IEngine, MappingAllocator { IEffect effect, uint16 stepsBitmap, bytes32 data, - uint96 slotIndex + uint96 slotIndex, + uint256 p0ActiveMonIndex, + uint256 p1ActiveMonIndex ) private { // Use stored bitmap instead of external call to shouldRunAtStep() if ((stepsBitmap & (1 << uint8(round))) == 0) { @@ -1204,45 +1451,59 @@ contract Engine is IEngine, MappingAllocator { // Emit event first, then handle side effects (use transient battleKeyForWrite) emit EffectRun( - battleKeyForWrite, effectIndex, monIndex, address(effect), data, _getUpstreamCallerAndResetValue(), currentStep + battleKeyForWrite, + effectIndex, + monIndex, + address(effect), + data, + _getUpstreamCallerAndResetValue(), + currentStep ); // Run the effect and get result - (bytes32 updatedExtraData, bool removeAfterRun) = _executeEffectHook( - effect, rng, data, playerIndex, monIndex, round, extraEffectsData - ); + (bytes32 updatedExtraData, bool removeAfterRun) = + _executeEffectHook(battleKeyForWrite, effect, rng, data, playerIndex, monIndex, round, extraEffectsData, p0ActiveMonIndex, p1ActiveMonIndex); // If we need to remove or update the effect if (removeAfterRun || updatedExtraData != data) { - _updateOrRemoveEffect(config, effectIndex, monIndex, effect, data, slotIndex, updatedExtraData, removeAfterRun); + _updateOrRemoveEffect( + config, effectIndex, monIndex, effect, data, slotIndex, updatedExtraData, removeAfterRun + ); } } function _executeEffectHook( + bytes32 battleKey, IEffect effect, uint256 rng, bytes32 data, uint256 playerIndex, uint256 monIndex, EffectStep round, - bytes memory extraEffectsData + bytes memory extraEffectsData, + uint256 p0ActiveMonIndex, + uint256 p1ActiveMonIndex ) private returns (bytes32 updatedExtraData, bool removeAfterRun) { if (round == EffectStep.RoundStart) { - return effect.onRoundStart(rng, data, playerIndex, monIndex); + return effect.onRoundStart(battleKey, rng, data, playerIndex, monIndex, p0ActiveMonIndex, p1ActiveMonIndex); } else if (round == EffectStep.RoundEnd) { - return effect.onRoundEnd(rng, data, playerIndex, monIndex); + return effect.onRoundEnd(battleKey, rng, data, playerIndex, monIndex, p0ActiveMonIndex, p1ActiveMonIndex); } else if (round == EffectStep.OnMonSwitchIn) { - return effect.onMonSwitchIn(rng, data, playerIndex, monIndex); + return effect.onMonSwitchIn(battleKey, rng, data, playerIndex, monIndex, p0ActiveMonIndex, p1ActiveMonIndex); } else if (round == EffectStep.OnMonSwitchOut) { - return effect.onMonSwitchOut(rng, data, playerIndex, monIndex); + return effect.onMonSwitchOut(battleKey, rng, data, playerIndex, monIndex, p0ActiveMonIndex, p1ActiveMonIndex); } else if (round == EffectStep.AfterDamage) { - return effect.onAfterDamage(rng, data, playerIndex, monIndex, abi.decode(extraEffectsData, (int32))); + return + effect.onAfterDamage(battleKey, rng, data, playerIndex, monIndex, p0ActiveMonIndex, p1ActiveMonIndex, abi.decode(extraEffectsData, (int32))); } else if (round == EffectStep.AfterMove) { - return effect.onAfterMove(rng, data, playerIndex, monIndex); + return effect.onAfterMove(battleKey, rng, data, playerIndex, monIndex, p0ActiveMonIndex, p1ActiveMonIndex); } else if (round == EffectStep.OnUpdateMonState) { (uint256 statePlayerIndex, uint256 stateMonIndex, MonStateIndexName stateVarIndex, int32 valueToAdd) = abi.decode(extraEffectsData, (uint256, uint256, MonStateIndexName, int32)); - return effect.onUpdateMonState(rng, data, statePlayerIndex, stateMonIndex, stateVarIndex, valueToAdd); + return + effect.onUpdateMonState( + battleKey, rng, data, statePlayerIndex, stateMonIndex, p0ActiveMonIndex, p1ActiveMonIndex, stateVarIndex, valueToAdd + ); } } @@ -1290,7 +1551,8 @@ contract Engine is IEngine, MappingAllocator { // If non-global effect, check if we should still run if mon is KOed if (effectIndex != 2) { bool isMonKOed = - _getMonState(config, playerIndex, _unpackActiveMonIndex(battle.activeMonIndex, playerIndex)).isKnockedOut; + _getMonState(config, playerIndex, _unpackActiveMonIndex(battle.activeMonIndex, playerIndex)) + .isKnockedOut; if (isMonKOed && condition == EffectRunCondition.SkipIfGameOverOrMonKO) { return playerSwitchForTurnFlag; } @@ -1305,7 +1567,9 @@ contract Engine is IEngine, MappingAllocator { } function computePriorityPlayerIndex(bytes32 battleKey, uint256 rng) public view returns (uint256) { - BattleConfig storage config = battleConfig[_getStorageKey(battleKey)]; + // Use cached storage key if available (during execute), otherwise compute + bytes32 storageKey = storageKeyForWrite != bytes32(0) ? storageKeyForWrite : _getStorageKey(battleKey); + BattleConfig storage config = battleConfig[storageKey]; BattleData storage battle = battleData[battleKey]; // Unpack move indices from packed format @@ -1350,10 +1614,12 @@ contract Engine is IEngine, MappingAllocator { int32 p0SpeedDelta = _getMonState(config, 0, p0ActiveMonIndex).speedDelta; int32 p1SpeedDelta = _getMonState(config, 1, p1ActiveMonIndex).speedDelta; uint32 p0MonSpeed = uint32( - int32(_getTeamMon(config, 0, p0ActiveMonIndex).stats.speed) + (p0SpeedDelta == CLEARED_MON_STATE_SENTINEL ? int32(0) : p0SpeedDelta) + int32(_getTeamMon(config, 0, p0ActiveMonIndex).stats.speed) + + (p0SpeedDelta == CLEARED_MON_STATE_SENTINEL ? int32(0) : p0SpeedDelta) ); uint32 p1MonSpeed = uint32( - int32(_getTeamMon(config, 1, p1ActiveMonIndex).stats.speed) + (p1SpeedDelta == CLEARED_MON_STATE_SENTINEL ? int32(0) : p1SpeedDelta) + int32(_getTeamMon(config, 1, p1ActiveMonIndex).stats.speed) + + (p1SpeedDelta == CLEARED_MON_STATE_SENTINEL ? int32(0) : p1SpeedDelta) ); if (p0MonSpeed > p1MonSpeed) { return 0; @@ -1412,11 +1678,19 @@ contract Engine is IEngine, MappingAllocator { } // Helper functions for accessing team and monState mappings - function _getTeamMon(BattleConfig storage config, uint256 playerIndex, uint256 monIndex) private view returns (Mon storage) { + function _getTeamMon(BattleConfig storage config, uint256 playerIndex, uint256 monIndex) + private + view + returns (Mon storage) + { return playerIndex == 0 ? config.p0Team[monIndex] : config.p1Team[monIndex]; } - function _getMonState(BattleConfig storage config, uint256 playerIndex, uint256 monIndex) private view returns (MonState storage) { + function _getMonState(BattleConfig storage config, uint256 playerIndex, uint256 monIndex) + private + view + returns (MonState storage) + { return playerIndex == 0 ? config.p0States[monIndex] : config.p1States[monIndex]; } @@ -1459,11 +1733,16 @@ contract Engine is IEngine, MappingAllocator { EffectInstance[] memory globalResult = new EffectInstance[](globalEffectsLength); uint256[] memory globalIndices = new uint256[](globalEffectsLength); uint256 globalIdx = 0; - for (uint256 i = 0; i < globalEffectsLength; ++i) { + for (uint256 i = 0; i < globalEffectsLength;) { if (address(config.globalEffects[i].effect) != TOMBSTONE_ADDRESS) { globalResult[globalIdx] = config.globalEffects[i]; globalIndices[globalIdx] = i; - globalIdx++; + unchecked { + ++globalIdx; + } + } + unchecked { + ++i; } } // Resize arrays to actual count @@ -1483,12 +1762,17 @@ contract Engine is IEngine, MappingAllocator { EffectInstance[] memory result = new EffectInstance[](monEffectCount); uint256[] memory indices = new uint256[](monEffectCount); uint256 idx = 0; - for (uint256 i = 0; i < monEffectCount; ++i) { + for (uint256 i = 0; i < monEffectCount;) { uint256 slotIndex = baseSlot + i; if (address(effects[slotIndex].effect) != TOMBSTONE_ADDRESS) { result[idx] = effects[slotIndex]; indices[idx] = slotIndex; - idx++; + unchecked { + ++idx; + } + } + unchecked { + ++i; } } @@ -1512,10 +1796,15 @@ contract Engine is IEngine, MappingAllocator { uint256 globalLen = config.globalEffectsLength; EffectInstance[] memory globalEffects = new EffectInstance[](globalLen); uint256 gIdx = 0; - for (uint256 i = 0; i < globalLen; ++i) { + for (uint256 i = 0; i < globalLen;) { if (address(config.globalEffects[i].effect) != TOMBSTONE_ADDRESS) { globalEffects[gIdx] = config.globalEffects[i]; - gIdx++; + unchecked { + ++gIdx; + } + } + unchecked { + ++i; } } // Resize array to actual count @@ -1528,29 +1817,43 @@ contract Engine is IEngine, MappingAllocator { uint256 p0TeamSize = teamSizes & 0xF; uint256 p1TeamSize = (teamSizes >> 4) & 0xF; - EffectInstance[][] memory p0Effects = _buildPlayerEffectsArray(config.p0Effects, config.packedP0EffectsCount, p0TeamSize); - EffectInstance[][] memory p1Effects = _buildPlayerEffectsArray(config.p1Effects, config.packedP1EffectsCount, p1TeamSize); + EffectInstance[][] memory p0Effects = + _buildPlayerEffectsArray(config.p0Effects, config.packedP0EffectsCount, p0TeamSize); + EffectInstance[][] memory p1Effects = + _buildPlayerEffectsArray(config.p1Effects, config.packedP1EffectsCount, p1TeamSize); // Build teams array from mappings Mon[][] memory teams = new Mon[][](2); teams[0] = new Mon[](p0TeamSize); teams[1] = new Mon[](p1TeamSize); - for (uint256 i = 0; i < p0TeamSize; i++) { + for (uint256 i = 0; i < p0TeamSize;) { teams[0][i] = config.p0Team[i]; + unchecked { + ++i; + } } - for (uint256 i = 0; i < p1TeamSize; i++) { + for (uint256 i = 0; i < p1TeamSize;) { teams[1][i] = config.p1Team[i]; + unchecked { + ++i; + } } // Build monStates array from mappings MonState[][] memory monStates = new MonState[][](2); monStates[0] = new MonState[](p0TeamSize); monStates[1] = new MonState[](p1TeamSize); - for (uint256 i = 0; i < p0TeamSize; i++) { + for (uint256 i = 0; i < p0TeamSize;) { monStates[0][i] = config.p0States[i]; + unchecked { + ++i; + } } - for (uint256 i = 0; i < p1TeamSize; i++) { + for (uint256 i = 0; i < p1TeamSize;) { monStates[1][i] = config.p1States[i]; + unchecked { + ++i; + } } BattleConfigView memory configView = BattleConfigView({ @@ -1583,17 +1886,22 @@ contract Engine is IEngine, MappingAllocator { // Allocate outer array for each mon EffectInstance[][] memory result = new EffectInstance[][](teamSize); - for (uint256 m = 0; m < teamSize; m++) { + for (uint256 m = 0; m < teamSize;) { uint256 monCount = _getMonEffectCount(packedCounts, m); uint256 baseSlot = _getEffectSlotIndex(m, 0); // Allocate max size for this mon's effects EffectInstance[] memory monEffects = new EffectInstance[](monCount); uint256 idx = 0; - for (uint256 i = 0; i < monCount; ++i) { + for (uint256 i = 0; i < monCount;) { if (address(effects[baseSlot + i].effect) != TOMBSTONE_ADDRESS) { monEffects[idx] = effects[baseSlot + i]; - idx++; + unchecked { + ++idx; + } + } + unchecked { + ++i; } } @@ -1602,6 +1910,9 @@ contract Engine is IEngine, MappingAllocator { mstore(monEffects, idx) } result[m] = monEffects; + unchecked { + ++m; + } } return result; @@ -1898,17 +2209,23 @@ contract Engine is IEngine, MappingAllocator { Mon storage attackerMon = _getTeamMon(config, attackerPlayerIndex, attackerMonIndex); MonState storage attackerState = _getMonState(config, attackerPlayerIndex, attackerMonIndex); ctx.attackerAttack = attackerMon.stats.attack; - ctx.attackerAttackDelta = attackerState.attackDelta == CLEARED_MON_STATE_SENTINEL ? int32(0) : attackerState.attackDelta; + ctx.attackerAttackDelta = + attackerState.attackDelta == CLEARED_MON_STATE_SENTINEL ? int32(0) : attackerState.attackDelta; ctx.attackerSpAtk = attackerMon.stats.specialAttack; - ctx.attackerSpAtkDelta = attackerState.specialAttackDelta == CLEARED_MON_STATE_SENTINEL ? int32(0) : attackerState.specialAttackDelta; + ctx.attackerSpAtkDelta = attackerState.specialAttackDelta == CLEARED_MON_STATE_SENTINEL + ? int32(0) + : attackerState.specialAttackDelta; // Get defender stats and types Mon storage defenderMon = _getTeamMon(config, defenderPlayerIndex, defenderMonIndex); MonState storage defenderState = _getMonState(config, defenderPlayerIndex, defenderMonIndex); ctx.defenderDef = defenderMon.stats.defense; - ctx.defenderDefDelta = defenderState.defenceDelta == CLEARED_MON_STATE_SENTINEL ? int32(0) : defenderState.defenceDelta; + ctx.defenderDefDelta = + defenderState.defenceDelta == CLEARED_MON_STATE_SENTINEL ? int32(0) : defenderState.defenceDelta; ctx.defenderSpDef = defenderMon.stats.specialDefense; - ctx.defenderSpDefDelta = defenderState.specialDefenceDelta == CLEARED_MON_STATE_SENTINEL ? int32(0) : defenderState.specialDefenceDelta; + ctx.defenderSpDefDelta = defenderState.specialDefenceDelta == CLEARED_MON_STATE_SENTINEL + ? int32(0) + : defenderState.specialDefenceDelta; ctx.defenderType1 = defenderMon.stats.type1; ctx.defenderType2 = defenderMon.stats.type2; } @@ -1937,8 +2254,10 @@ contract Engine is IEngine, MappingAllocator { Mon storage p0Mon = config.p0Team[p0MonIndex]; Mon storage p1Mon = config.p1Team[p1MonIndex]; ctx.p0ActiveMonBaseStamina = p0Mon.stats.stamina; - ctx.p0ActiveMonStaminaDelta = p0State.staminaDelta == CLEARED_MON_STATE_SENTINEL ? int32(0) : p0State.staminaDelta; + ctx.p0ActiveMonStaminaDelta = + p0State.staminaDelta == CLEARED_MON_STATE_SENTINEL ? int32(0) : p0State.staminaDelta; ctx.p1ActiveMonBaseStamina = p1Mon.stats.stamina; - ctx.p1ActiveMonStaminaDelta = p1State.staminaDelta == CLEARED_MON_STATE_SENTINEL ? int32(0) : p1State.staminaDelta; + ctx.p1ActiveMonStaminaDelta = + p1State.staminaDelta == CLEARED_MON_STATE_SENTINEL ? int32(0) : p1State.staminaDelta; } } diff --git a/src/commit-manager/DefaultCommitManager.sol b/src/commit-manager/DefaultCommitManager.sol index 04502420..daf70b29 100644 --- a/src/commit-manager/DefaultCommitManager.sol +++ b/src/commit-manager/DefaultCommitManager.sol @@ -214,9 +214,11 @@ contract DefaultCommitManager is ICommitManager { // 5) Validate that the commited moves are legal // (e.g. there is enough stamina, move is not disabled, etc.) - // Use validator from context instead of calling getBattleValidator - if (!IValidator(ctx.validator).validatePlayerMove(battleKey, moveIndex, currentPlayerIndex, extraData)) { - revert InvalidMove(msg.sender); + // Skip if validator is address(0) - validation will happen inline in Engine.execute() + if (ctx.validator != address(0)) { + if (!IValidator(ctx.validator).validatePlayerMove(battleKey, moveIndex, currentPlayerIndex, extraData)) { + revert InvalidMove(msg.sender); + } } // 6) Store revealed move and extra data for the current player diff --git a/src/effects/BasicEffect.sol b/src/effects/BasicEffect.sol index ff6a0024..13f89562 100644 --- a/src/effects/BasicEffect.sol +++ b/src/effects/BasicEffect.sol @@ -15,12 +15,13 @@ abstract contract BasicEffect is IEffect { } // Whether or not to add the effect if the step condition is met - function shouldApply(bytes32, uint256, uint256) external virtual returns (bool) { + function shouldApply(bytes32, bytes32, uint256, uint256) external virtual returns (bool) { return true; } // Lifecycle hooks during normal battle flow - function onRoundStart(uint256, bytes32 extraData, uint256, uint256) + // p0ActiveMonIndex and p1ActiveMonIndex are passed to avoid external calls back to Engine + function onRoundStart(bytes32, uint256, bytes32 extraData, uint256, uint256, uint256, uint256) external virtual returns (bytes32 updatedExtraData, bool removeAfterRun) @@ -28,7 +29,7 @@ abstract contract BasicEffect is IEffect { return (extraData, false); } - function onRoundEnd(uint256, bytes32 extraData, uint256, uint256) + function onRoundEnd(bytes32, uint256, bytes32 extraData, uint256, uint256, uint256, uint256) external virtual returns (bytes32 updatedExtraData, bool removeAfterRun) @@ -37,7 +38,7 @@ abstract contract BasicEffect is IEffect { } // NOTE: ONLY RUN ON GLOBAL EFFECTS (mons have their Ability as their own hook to apply an effect on switch in) - function onMonSwitchIn(uint256, bytes32 extraData, uint256, uint256) + function onMonSwitchIn(bytes32, uint256, bytes32 extraData, uint256, uint256, uint256, uint256) external virtual returns (bytes32 updatedExtraData, bool removeAfterRun) @@ -46,7 +47,7 @@ abstract contract BasicEffect is IEffect { } // NOTE: CURRENTLY ONLY RUN LOCALLY ON MONS (global effects do not have this hook) - function onMonSwitchOut(uint256, bytes32 extraData, uint256, uint256) + function onMonSwitchOut(bytes32, uint256, bytes32 extraData, uint256, uint256, uint256, uint256) external virtual returns (bytes32 updatedExtraData, bool removeAfterRun) @@ -55,7 +56,7 @@ abstract contract BasicEffect is IEffect { } // NOTE: CURRENTLY ONLY RUN LOCALLY ON MONS (global effects do not have this hook) - function onAfterDamage(uint256, bytes32 extraData, uint256, uint256, int32) + function onAfterDamage(bytes32, uint256, bytes32 extraData, uint256, uint256, uint256, uint256, int32) external virtual returns (bytes32 updatedExtraData, bool removeAfterRun) @@ -63,7 +64,7 @@ abstract contract BasicEffect is IEffect { return (extraData, false); } - function onAfterMove(uint256, bytes32 extraData, uint256, uint256) + function onAfterMove(bytes32, uint256, bytes32 extraData, uint256, uint256, uint256, uint256) external virtual returns (bytes32 updatedExtraData, bool removeAfterRun) @@ -73,7 +74,7 @@ abstract contract BasicEffect is IEffect { // NOTE: CURRENTLY ONLY RUN LOCALLY ON MONS (global effects do not have this hook) // WARNING: Avoid chaining this effect to prevent recursive calls - function onUpdateMonState(uint256, bytes32 extraData, uint256, uint256, MonStateIndexName, int32) + function onUpdateMonState(bytes32, uint256, bytes32 extraData, uint256, uint256, uint256, uint256, MonStateIndexName, int32) external virtual returns (bytes32 updatedExtraData, bool removeAfterRun) @@ -82,7 +83,7 @@ abstract contract BasicEffect is IEffect { } // Lifecycle hooks when being applied or removed - function onApply(uint256, bytes32, uint256, uint256) + function onApply(bytes32, uint256, bytes32, uint256, uint256, uint256, uint256) external virtual returns (bytes32 updatedExtraData, bool removeAfterRun) @@ -90,5 +91,5 @@ abstract contract BasicEffect is IEffect { return (updatedExtraData, removeAfterRun); } - function onRemove(bytes32 extraData, uint256 targetIndex, uint256 monIndex) external virtual {} + function onRemove(bytes32, bytes32, uint256, uint256, uint256, uint256) external virtual {} } diff --git a/src/effects/IEffect.sol b/src/effects/IEffect.sol index 5d4ec3b2..aaf940df 100644 --- a/src/effects/IEffect.sol +++ b/src/effects/IEffect.sol @@ -13,48 +13,104 @@ interface IEffect { function getStepsBitmap() external view returns (uint16); // Whether or not to add the effect if some condition is met - function shouldApply(bytes32 extraData, uint256 targetIndex, uint256 monIndex) external returns (bool); + function shouldApply(bytes32 battleKey, bytes32 extraData, uint256 targetIndex, uint256 monIndex) external returns (bool); // Lifecycle hooks during normal battle flow - function onRoundStart(uint256 rng, bytes32 extraData, uint256 targetIndex, uint256 monIndex) - external - returns (bytes32 updatedExtraData, bool removeAfterRun); - function onRoundEnd(uint256 rng, bytes32 extraData, uint256 targetIndex, uint256 monIndex) - external - returns (bytes32 updatedExtraData, bool removeAfterRun); + // p0ActiveMonIndex and p1ActiveMonIndex are passed to avoid external calls back to Engine + function onRoundStart( + bytes32 battleKey, + uint256 rng, + bytes32 extraData, + uint256 targetIndex, + uint256 monIndex, + uint256 p0ActiveMonIndex, + uint256 p1ActiveMonIndex + ) external returns (bytes32 updatedExtraData, bool removeAfterRun); + + function onRoundEnd( + bytes32 battleKey, + uint256 rng, + bytes32 extraData, + uint256 targetIndex, + uint256 monIndex, + uint256 p0ActiveMonIndex, + uint256 p1ActiveMonIndex + ) external returns (bytes32 updatedExtraData, bool removeAfterRun); - function onMonSwitchIn(uint256 rng, bytes32 extraData, uint256 targetIndex, uint256 monIndex) - external - returns (bytes32 updatedExtraData, bool removeAfterRun); + function onMonSwitchIn( + bytes32 battleKey, + uint256 rng, + bytes32 extraData, + uint256 targetIndex, + uint256 monIndex, + uint256 p0ActiveMonIndex, + uint256 p1ActiveMonIndex + ) external returns (bytes32 updatedExtraData, bool removeAfterRun); - function onMonSwitchOut(uint256 rng, bytes32 extraData, uint256 targetIndex, uint256 monIndex) - external - returns (bytes32 updatedExtraData, bool removeAfterRun); + function onMonSwitchOut( + bytes32 battleKey, + uint256 rng, + bytes32 extraData, + uint256 targetIndex, + uint256 monIndex, + uint256 p0ActiveMonIndex, + uint256 p1ActiveMonIndex + ) external returns (bytes32 updatedExtraData, bool removeAfterRun); // NOTE: CURRENTLY ONLY RUN LOCALLY ON MONS (global effects do not have this hook) - function onAfterDamage(uint256 rng, bytes32 extraData, uint256 targetIndex, uint256 monIndex, int32 damage) - external - returns (bytes32 updatedExtraData, bool removeAfterRun); + function onAfterDamage( + bytes32 battleKey, + uint256 rng, + bytes32 extraData, + uint256 targetIndex, + uint256 monIndex, + uint256 p0ActiveMonIndex, + uint256 p1ActiveMonIndex, + int32 damage + ) external returns (bytes32 updatedExtraData, bool removeAfterRun); - function onAfterMove(uint256 rng, bytes32 extraData, uint256 targetIndex, uint256 monIndex) - external - returns (bytes32 updatedExtraData, bool removeAfterRun); + function onAfterMove( + bytes32 battleKey, + uint256 rng, + bytes32 extraData, + uint256 targetIndex, + uint256 monIndex, + uint256 p0ActiveMonIndex, + uint256 p1ActiveMonIndex + ) external returns (bytes32 updatedExtraData, bool removeAfterRun); // NOTE: CURRENTLY ONLY RUN LOCALLY ON MONS (global effects do not have this hook) // WARNING: Avoid chaining this effect to prevent recursive calls // (e.g., an effect that mutates state triggering another effect that mutates state) function onUpdateMonState( + bytes32 battleKey, uint256 rng, bytes32 extraData, uint256 playerIndex, uint256 monIndex, + uint256 p0ActiveMonIndex, + uint256 p1ActiveMonIndex, MonStateIndexName stateVarIndex, int32 valueToAdd ) external returns (bytes32 updatedExtraData, bool removeAfterRun); // Lifecycle hooks when being applied or removed - function onApply(uint256 rng, bytes32 extraData, uint256 targetIndex, uint256 monIndex) - external - returns (bytes32 updatedExtraData, bool removeAfterRun); - function onRemove(bytes32 extraData, uint256 targetIndex, uint256 monIndex) external; + function onApply( + bytes32 battleKey, + uint256 rng, + bytes32 extraData, + uint256 targetIndex, + uint256 monIndex, + uint256 p0ActiveMonIndex, + uint256 p1ActiveMonIndex + ) external returns (bytes32 updatedExtraData, bool removeAfterRun); + + function onRemove( + bytes32 battleKey, + bytes32 extraData, + uint256 targetIndex, + uint256 monIndex, + uint256 p0ActiveMonIndex, + uint256 p1ActiveMonIndex + ) external; } diff --git a/src/effects/StaminaRegen.sol b/src/effects/StaminaRegen.sol index 00e56e76..e8a6801c 100644 --- a/src/effects/StaminaRegen.sol +++ b/src/effects/StaminaRegen.sol @@ -25,40 +25,52 @@ contract StaminaRegen is BasicEffect { } // No overhealing stamina - function _regenStamina(uint256 playerIndex, uint256 monIndex) internal { + function _regenStamina(bytes32 battleKey, uint256 playerIndex, uint256 monIndex) internal { int256 currentActiveMonStaminaDelta = - ENGINE.getMonStateForBattle(ENGINE.battleKeyForWrite(), playerIndex, monIndex, MonStateIndexName.Stamina); + ENGINE.getMonStateForBattle(battleKey, playerIndex, monIndex, MonStateIndexName.Stamina); if (currentActiveMonStaminaDelta < 0) { ENGINE.updateMonState(playerIndex, monIndex, MonStateIndexName.Stamina, 1); } } // Regen stamina on round end for both active mons - function onRoundEnd(uint256, bytes32, uint256, uint256) external override returns (bytes32, bool) { - bytes32 battleKey = ENGINE.battleKeyForWrite(); + function onRoundEnd( + bytes32 battleKey, + uint256, + bytes32, + uint256, + uint256, + uint256 p0ActiveMonIndex, + uint256 p1ActiveMonIndex + ) external override returns (bytes32, bool) { uint256 playerSwitchForTurnFlag = ENGINE.getPlayerSwitchForTurnFlagForBattleState(battleKey); - uint256[] memory activeMonIndex = ENGINE.getActiveMonIndexForBattleState(battleKey); // Update stamina for both active mons only if it's a 2 player turn if (playerSwitchForTurnFlag == 2) { - for (uint256 playerIndex; playerIndex < 2; ++playerIndex) { - _regenStamina(playerIndex, activeMonIndex[playerIndex]); - } + _regenStamina(battleKey, 0, p0ActiveMonIndex); + _regenStamina(battleKey, 1, p1ActiveMonIndex); } return (bytes32(0), false); } // Regen stamina if the mon did a No Op (i.e. resting) - function onAfterMove(uint256, bytes32, uint256 targetIndex, uint256 monIndex) + function onAfterMove( + bytes32 battleKey, + uint256, + bytes32, + uint256 targetIndex, + uint256 monIndex, + uint256, + uint256 + ) external override returns (bytes32, bool) { - bytes32 battleKey = ENGINE.battleKeyForWrite(); MoveDecision memory moveDecision = ENGINE.getMoveDecisionForBattleState(battleKey, targetIndex); // Unpack the move index from packedMoveIndex uint8 moveIndex = moveDecision.packedMoveIndex & MOVE_INDEX_MASK; if (moveIndex == NO_OP_MOVE_INDEX) { - _regenStamina(targetIndex, monIndex); + _regenStamina(battleKey, targetIndex, monIndex); } return (bytes32(0), false); } diff --git a/src/effects/StatBoosts.sol b/src/effects/StatBoosts.sol index 781ee7a5..d148cd56 100644 --- a/src/effects/StatBoosts.sol +++ b/src/effects/StatBoosts.sol @@ -46,7 +46,15 @@ contract StatBoosts is BasicEffect { } // Removes all temporary boosts on mon switch out - function onMonSwitchOut(uint256, bytes32 extraData, uint256 targetIndex, uint256 monIndex) + function onMonSwitchOut( + bytes32 battleKey, + uint256, + bytes32 extraData, + uint256 targetIndex, + uint256 monIndex, + uint256, + uint256 + ) external override returns (bytes32, bool) @@ -56,7 +64,7 @@ contract StatBoosts is BasicEffect { if (!isPerm) { // This is a temp boost, remove it and recalculate stats // Pass excludeTempBoosts=true since all temp boosts are being removed - _recalculateAndApplyStats(targetIndex, monIndex, true); + _recalculateAndApplyStats(battleKey, targetIndex, monIndex, true); return (extraData, true); // Remove this effect } return (extraData, false); @@ -253,8 +261,7 @@ contract StatBoosts is BasicEffect { // Recalculate stats by iterating through all StatBoosts effects // If excludeTempBoosts is true, skip temp boosts (used during onMonSwitchOut when temp boosts are being removed) - function _recalculateAndApplyStats(uint256 targetIndex, uint256 monIndex, bool excludeTempBoosts) internal { - bytes32 battleKey = ENGINE.battleKeyForWrite(); + function _recalculateAndApplyStats(bytes32 battleKey, uint256 targetIndex, uint256 monIndex, bool excludeTempBoosts) internal { bytes32 snapshotKey = _snapshotKey(targetIndex, monIndex); uint192 prevSnapshot = ENGINE.getGlobalKV(battleKey, snapshotKey); diff --git a/src/effects/battlefield/Overclock.sol b/src/effects/battlefield/Overclock.sol index 390be715..8f1f85e0 100644 --- a/src/effects/battlefield/Overclock.sol +++ b/src/effects/battlefield/Overclock.sol @@ -36,9 +36,8 @@ contract Overclock is BasicEffect { return keccak256(abi.encode(playerIndex, name())); } - function applyOverclock(uint256 playerIndex) public { + function applyOverclock(bytes32 battleKey, uint256 playerIndex) public { // Check if we have an active Overclock effect - bytes32 battleKey = ENGINE.battleKeyForWrite(); uint256 duration = getDuration(battleKey, playerIndex); if (duration == 0) { // If not, add the effect to the global effects array @@ -78,7 +77,15 @@ contract Overclock is BasicEffect { STAT_BOOST.removeStatBoosts(playerIndex, monIndex, StatBoostFlag.Temp); } - function onApply(uint256, bytes32 extraData, uint256, uint256) + function onApply( + bytes32, + uint256, + bytes32 extraData, + uint256, + uint256, + uint256 p0ActiveMonIndex, + uint256 p1ActiveMonIndex + ) external override returns (bytes32 updatedExtraData, bool removeAfterRun) @@ -89,19 +96,27 @@ contract Overclock is BasicEffect { setDuration(DEFAULT_DURATION, playerIndex); // Apply stat change to the team of the player who summoned Overclock - uint256 activeMonIndex = ENGINE.getActiveMonIndexForBattleState(ENGINE.battleKeyForWrite())[playerIndex]; + uint256 activeMonIndex = playerIndex == 0 ? p0ActiveMonIndex : p1ActiveMonIndex; _applyStatChange(playerIndex, activeMonIndex); return (extraData, false); } - function onRoundEnd(uint256, bytes32 extraData, uint256, uint256) + function onRoundEnd( + bytes32 battleKey, + uint256, + bytes32 extraData, + uint256, + uint256, + uint256, + uint256 + ) external override returns (bytes32 updatedExtraData, bool removeAfterRun) { uint256 playerIndex = uint256(extraData); - uint256 duration = getDuration(ENGINE.battleKeyForWrite(), playerIndex); + uint256 duration = getDuration(battleKey, playerIndex); if (duration == 1) { return (extraData, true); } else { @@ -110,7 +125,15 @@ contract Overclock is BasicEffect { } } - function onMonSwitchIn(uint256, bytes32 extraData, uint256 targetIndex, uint256 monIndex) + function onMonSwitchIn( + bytes32, + uint256, + bytes32 extraData, + uint256 targetIndex, + uint256 monIndex, + uint256, + uint256 + ) external override returns (bytes32 updatedExtraData, bool removeAfterRun) @@ -123,13 +146,27 @@ contract Overclock is BasicEffect { return (extraData, false); } - function onRemove(bytes32 extraData, uint256, uint256) external override { + function onRoundStart(bytes32, uint256, bytes32 extraData, uint256, uint256, uint256, uint256) + external + override + returns (bytes32 updatedExtraData, bool removeAfterRun) + { + return (extraData, false); + } + + function onRemove( + bytes32, + bytes32 extraData, + uint256, + uint256, + uint256 p0ActiveMonIndex, + uint256 p1ActiveMonIndex + ) external override { uint256 playerIndex = uint256(extraData); - uint256 activeMonIndex = ENGINE.getActiveMonIndexForBattleState(ENGINE.battleKeyForWrite())[playerIndex]; + uint256 activeMonIndex = playerIndex == 0 ? p0ActiveMonIndex : p1ActiveMonIndex; // Reset stat changes from the mon on the team of the player who summoned Overclock _removeStatChange(playerIndex, activeMonIndex); // Clear the duration when we clear the effect setDuration(0, playerIndex); } } - diff --git a/src/effects/status/BurnStatus.sol b/src/effects/status/BurnStatus.sol index 3c3bf1cb..75d67def 100644 --- a/src/effects/status/BurnStatus.sol +++ b/src/effects/status/BurnStatus.sol @@ -34,8 +34,7 @@ contract BurnStatus is StatusEffect { return 0x0F; } - function shouldApply(bytes32, uint256 targetIndex, uint256 monIndex) public view override returns (bool) { - bytes32 battleKey = ENGINE.battleKeyForWrite(); + function shouldApply(bytes32 battleKey, bytes32, uint256 targetIndex, uint256 monIndex) public view override returns (bool) { bytes32 keyForMon = StatusEffectLib.getKeyForMonIndex(targetIndex, monIndex); // Get value from ENGINE KV @@ -51,12 +50,19 @@ contract BurnStatus is StatusEffect { return keccak256(abi.encode(targetIndex, monIndex, name())); } - function onApply(uint256 rng, bytes32, uint256 targetIndex, uint256 monIndex) + function onApply( + bytes32 battleKey, + uint256 rng, + bytes32, + uint256 targetIndex, + uint256 monIndex, + uint256 p0ActiveMonIndex, + uint256 p1ActiveMonIndex + ) public override returns (bytes32 updatedExtraData, bool removeAfterRun) { - bytes32 battleKey = ENGINE.battleKeyForWrite(); bool hasBurnAlready; { bytes32 keyForMon = StatusEffectLib.getKeyForMonIndex(targetIndex, monIndex); @@ -65,7 +71,7 @@ contract BurnStatus is StatusEffect { } // Set burn flag - super.onApply(rng, bytes32(0), targetIndex, monIndex); + super.onApply(battleKey, rng, bytes32(0), targetIndex, monIndex, p0ActiveMonIndex, p1ActiveMonIndex); // Set stat debuff or increase burn degree if (!hasBurnAlready) { @@ -98,9 +104,16 @@ contract BurnStatus is StatusEffect { return (bytes32(uint256(1)), hasBurnAlready); } - function onRemove(bytes32, uint256 targetIndex, uint256 monIndex) public override { + function onRemove( + bytes32 battleKey, + bytes32, + uint256 targetIndex, + uint256 monIndex, + uint256 p0ActiveMonIndex, + uint256 p1ActiveMonIndex + ) public override { // Remove the base status flag - super.onRemove(bytes32(0), targetIndex, monIndex); + super.onRemove(battleKey, bytes32(0), targetIndex, monIndex, p0ActiveMonIndex, p1ActiveMonIndex); // Reset the attack reduction STAT_BOOSTS.removeStatBoosts(targetIndex, monIndex, StatBoostFlag.Perm); @@ -110,7 +123,15 @@ contract BurnStatus is StatusEffect { } // Deal damage over time - function onRoundEnd(uint256, bytes32 extraData, uint256 targetIndex, uint256 monIndex) + function onRoundEnd( + bytes32 battleKey, + uint256, + bytes32 extraData, + uint256 targetIndex, + uint256 monIndex, + uint256, + uint256 + ) external override returns (bytes32, bool) @@ -124,7 +145,7 @@ contract BurnStatus is StatusEffect { damageDenom = DEG3_DAMAGE_DENOM; } int32 damage = - int32(ENGINE.getMonValueForBattle(ENGINE.battleKeyForWrite(), targetIndex, monIndex, MonStateIndexName.Hp)) + int32(ENGINE.getMonValueForBattle(battleKey, targetIndex, monIndex, MonStateIndexName.Hp)) / damageDenom; ENGINE.dealDamage(targetIndex, monIndex, damage); return (extraData, false); diff --git a/src/effects/status/FrostbiteStatus.sol b/src/effects/status/FrostbiteStatus.sol index 91a6bb27..a56a0df7 100644 --- a/src/effects/status/FrostbiteStatus.sol +++ b/src/effects/status/FrostbiteStatus.sol @@ -28,13 +28,21 @@ contract FrostbiteStatus is StatusEffect { return 0x0D; } - function onApply(uint256 rng, bytes32 extraData, uint256 targetIndex, uint256 monIndex) + function onApply( + bytes32 battleKey, + uint256 rng, + bytes32 extraData, + uint256 targetIndex, + uint256 monIndex, + uint256 p0ActiveMonIndex, + uint256 p1ActiveMonIndex + ) public override returns (bytes32 updatedExtraData, bool removeAfterRun) { - super.onApply(rng, extraData, targetIndex, monIndex); + super.onApply(battleKey, rng, extraData, targetIndex, monIndex, p0ActiveMonIndex, p1ActiveMonIndex); // Reduce special attack by half StatBoostToApply[] memory statBoosts = new StatBoostToApply[](1); @@ -49,21 +57,36 @@ contract FrostbiteStatus is StatusEffect { return (extraData, false); } - function onRemove(bytes32 data, uint256 targetIndex, uint256 monIndex) public override { - super.onRemove(data, targetIndex, monIndex); + function onRemove( + bytes32 battleKey, + bytes32 data, + uint256 targetIndex, + uint256 monIndex, + uint256 p0ActiveMonIndex, + uint256 p1ActiveMonIndex + ) public override { + super.onRemove(battleKey, data, targetIndex, monIndex, p0ActiveMonIndex, p1ActiveMonIndex); // Reset the special attack reduction STAT_BOOST.removeStatBoosts(targetIndex, monIndex, StatBoostFlag.Perm); } - function onRoundEnd(uint256, bytes32 extraData, uint256 targetIndex, uint256 monIndex) + function onRoundEnd( + bytes32 battleKey, + uint256, + bytes32 extraData, + uint256 targetIndex, + uint256 monIndex, + uint256, + uint256 + ) public override returns (bytes32, bool) { // Calculate damage to deal uint32 maxHealth = - ENGINE.getMonValueForBattle(ENGINE.battleKeyForWrite(), targetIndex, monIndex, MonStateIndexName.Hp); + ENGINE.getMonValueForBattle(battleKey, targetIndex, monIndex, MonStateIndexName.Hp); int32 damage = int32(maxHealth) / DAMAGE_DENOM; ENGINE.dealDamage(targetIndex, monIndex, damage); diff --git a/src/effects/status/PanicStatus.sol b/src/effects/status/PanicStatus.sol index 25bef4e7..bbe6e5bd 100644 --- a/src/effects/status/PanicStatus.sol +++ b/src/effects/status/PanicStatus.sol @@ -21,7 +21,15 @@ contract PanicStatus is StatusEffect { } // At the start of the turn, check to see if we should apply stamina debuff or end early - function onRoundStart(uint256 rng, bytes32 extraData, uint256 targetIndex, uint256 monIndex) + function onRoundStart( + bytes32, + uint256 rng, + bytes32 extraData, + uint256 targetIndex, + uint256 monIndex, + uint256, + uint256 + ) external pure override @@ -37,23 +45,37 @@ contract PanicStatus is StatusEffect { } // On apply, checks to apply the flag, and then sets the extraData to be the duration - function onApply(uint256 rng, bytes32 data, uint256 monIndex, uint256 playerIndex) + function onApply( + bytes32 battleKey, + uint256 rng, + bytes32 data, + uint256 targetIndex, + uint256 monIndex, + uint256 p0ActiveMonIndex, + uint256 p1ActiveMonIndex + ) public override returns (bytes32 updatedExtraData, bool removeAfterRun) { - super.onApply(rng, data, monIndex, playerIndex); + super.onApply(battleKey, rng, data, targetIndex, monIndex, p0ActiveMonIndex, p1ActiveMonIndex); return (bytes32(DURATION), false); } // Apply effect on end of turn, and then check how many turns are left - function onRoundEnd(uint256, bytes32 extraData, uint256 targetIndex, uint256 monIndex) + function onRoundEnd( + bytes32 battleKey, + uint256, + bytes32 extraData, + uint256 targetIndex, + uint256 monIndex, + uint256, + uint256 + ) external override returns (bytes32, bool removeAfterRun) { - bytes32 battleKey = ENGINE.battleKeyForWrite(); - // Get current stamina delta of the target mon int32 staminaDelta = ENGINE.getMonStateForBattle(battleKey, targetIndex, monIndex, MonStateIndexName.Stamina); diff --git a/src/effects/status/SleepStatus.sol b/src/effects/status/SleepStatus.sol index b3daa391..f3041f1e 100644 --- a/src/effects/status/SleepStatus.sol +++ b/src/effects/status/SleepStatus.sol @@ -27,15 +27,14 @@ contract SleepStatus is StatusEffect { } // Whether or not to add the effect if the step condition is met - function shouldApply(bytes32 data, uint256 targetIndex, uint256 monIndex) public view override returns (bool) { - bool shouldApplyStatusInGeneral = super.shouldApply(data, targetIndex, monIndex); + function shouldApply(bytes32 battleKey, bytes32 data, uint256 targetIndex, uint256 monIndex) public view override returns (bool) { + bool shouldApplyStatusInGeneral = super.shouldApply(battleKey, data, targetIndex, monIndex); bool playerHasZeroSleepers = - address(uint160(ENGINE.getGlobalKV(ENGINE.battleKeyForWrite(), _globalSleepKey(targetIndex)))) == address(0); + address(uint160(ENGINE.getGlobalKV(battleKey, _globalSleepKey(targetIndex)))) == address(0); return (shouldApplyStatusInGeneral && playerHasZeroSleepers); } - function _applySleep(uint256 targetIndex, uint256) internal { - bytes32 battleKey = ENGINE.battleKeyForWrite(); + function _applySleep(bytes32 battleKey, uint256 targetIndex, uint256) internal { // Get exiting move index (unpack from packedMoveIndex) MoveDecision memory moveDecision = ENGINE.getMoveDecisionForBattleState(battleKey, targetIndex); uint8 moveIndex = moveDecision.packedMoveIndex & MOVE_INDEX_MASK; @@ -45,35 +44,50 @@ contract SleepStatus is StatusEffect { } // At the start of the turn, check to see if we should apply sleep or end early - function onRoundStart(uint256 rng, bytes32 extraData, uint256 targetIndex, uint256 monIndex) + function onRoundStart( + bytes32 battleKey, + uint256 rng, + bytes32 extraData, + uint256 targetIndex, + uint256 monIndex, + uint256, + uint256 + ) external override returns (bytes32, bool) { bool wakeEarly = rng % 3 == 0; if (!wakeEarly) { - _applySleep(targetIndex, monIndex); + _applySleep(battleKey, targetIndex, monIndex); } return (extraData, wakeEarly); } // On apply, checks to apply the sleep flag, and then sets the extraData to be the duration - function onApply(uint256 rng, bytes32 data, uint256 targetIndex, uint256 monIndex) + function onApply( + bytes32 battleKey, + uint256 rng, + bytes32 data, + uint256 targetIndex, + uint256 monIndex, + uint256 p0ActiveMonIndex, + uint256 p1ActiveMonIndex + ) public override returns (bytes32 updatedExtraData, bool removeAfterRun) { - super.onApply(rng, data, targetIndex, monIndex); + super.onApply(battleKey, rng, data, targetIndex, monIndex, p0ActiveMonIndex, p1ActiveMonIndex); // Check if opponent has yet to move and if so, also affect their move for this round - bytes32 battleKey = ENGINE.battleKeyForWrite(); uint256 priorityPlayerIndex = ENGINE.computePriorityPlayerIndex(battleKey, rng); if (targetIndex != priorityPlayerIndex) { - _applySleep(targetIndex, monIndex); + _applySleep(battleKey, targetIndex, monIndex); } return (bytes32(DURATION), false); } - function onRoundEnd(uint256, bytes32 extraData, uint256, uint256) + function onRoundEnd(bytes32, uint256, bytes32 extraData, uint256, uint256, uint256, uint256) external pure override @@ -87,8 +101,15 @@ contract SleepStatus is StatusEffect { } } - function onRemove(bytes32 extraData, uint256 targetIndex, uint256 monIndex) public override { - super.onRemove(extraData, targetIndex, monIndex); + function onRemove( + bytes32 battleKey, + bytes32 extraData, + uint256 targetIndex, + uint256 monIndex, + uint256 p0ActiveMonIndex, + uint256 p1ActiveMonIndex + ) public override { + super.onRemove(battleKey, extraData, targetIndex, monIndex, p0ActiveMonIndex, p1ActiveMonIndex); ENGINE.setGlobalKV(_globalSleepKey(targetIndex), 0); } } diff --git a/src/effects/status/StatusEffect.sol b/src/effects/status/StatusEffect.sol index e63b7c39..c40a858d 100644 --- a/src/effects/status/StatusEffect.sol +++ b/src/effects/status/StatusEffect.sol @@ -13,8 +13,7 @@ abstract contract StatusEffect is BasicEffect { } // Whether or not to add the effect if the step condition is met - function shouldApply(bytes32, uint256 targetIndex, uint256 monIndex) public virtual view override returns (bool) { - bytes32 battleKey = ENGINE.battleKeyForWrite(); + function shouldApply(bytes32 battleKey, bytes32, uint256 targetIndex, uint256 monIndex) public virtual view override returns (bool) { bytes32 keyForMon = StatusEffectLib.getKeyForMonIndex(targetIndex, monIndex); // Get value from ENGINE KV @@ -29,13 +28,12 @@ abstract contract StatusEffect is BasicEffect { } } - function onApply(uint256, bytes32, uint256 targetIndex, uint256 monIndex) + function onApply(bytes32 battleKey, uint256, bytes32, uint256 targetIndex, uint256 monIndex, uint256, uint256) public virtual override returns (bytes32, bool) { - bytes32 battleKey = ENGINE.battleKeyForWrite(); bytes32 keyForMon = StatusEffectLib.getKeyForMonIndex(targetIndex, monIndex); uint192 monValue = ENGINE.getGlobalKV(battleKey, keyForMon); @@ -45,7 +43,7 @@ abstract contract StatusEffect is BasicEffect { } } - function onRemove(bytes32, uint256 targetIndex, uint256 monIndex) public virtual override { + function onRemove(bytes32 battleKey, bytes32, uint256 targetIndex, uint256 monIndex, uint256, uint256) public virtual override { // On remove, reset the status flag ENGINE.setGlobalKV(StatusEffectLib.getKeyForMonIndex(targetIndex, monIndex), 0); } diff --git a/src/effects/status/ZapStatus.sol b/src/effects/status/ZapStatus.sol index 9e7a97ac..dcbac107 100644 --- a/src/effects/status/ZapStatus.sol +++ b/src/effects/status/ZapStatus.sol @@ -9,7 +9,7 @@ import {IEngine} from "../../IEngine.sol"; import {StatusEffect} from "./StatusEffect.sol"; contract ZapStatus is StatusEffect { - + uint8 private constant ALREADY_SKIPPED = 1; constructor(IEngine engine) StatusEffect(engine) {} @@ -23,15 +23,22 @@ contract ZapStatus is StatusEffect { return 0x0F; } - function onApply(uint256 rng, bytes32 data, uint256 targetIndex, uint256 monIndex) + function onApply( + bytes32 battleKey, + uint256 rng, + bytes32 extraData, + uint256 targetIndex, + uint256 monIndex, + uint256 p0ActiveMonIndex, + uint256 p1ActiveMonIndex + ) public override returns (bytes32 updatedExtraData, bool removeAfterRun) { - super.onApply(rng, data, targetIndex, monIndex); + super.onApply(battleKey, rng, extraData, targetIndex, monIndex, p0ActiveMonIndex, p1ActiveMonIndex); - // Get the battle key and compute priority player index - bytes32 battleKey = ENGINE.battleKeyForWrite(); + // Compute priority player index uint256 priorityPlayerIndex = ENGINE.computePriorityPlayerIndex(battleKey, rng); uint8 state; @@ -48,13 +55,20 @@ contract ZapStatus is StatusEffect { return (bytes32(uint256(state)), false); } - function onRoundStart(uint256, bytes32, uint256 targetIndex, uint256 monIndex) + function onRoundStart( + bytes32 battleKey, + uint256, + bytes32, + uint256 targetIndex, + uint256 monIndex, + uint256, + uint256 + ) external override returns (bytes32 updatedExtraData, bool removeAfterRun) { // If we're at RoundStart and effect is still present, always set skip flag and mark as skipped, unless the selected move is a switch move - bytes32 battleKey = ENGINE.battleKeyForWrite(); MoveDecision memory moveDecision = ENGINE.getMoveDecisionForBattleState(battleKey, targetIndex); uint8 moveIndex = moveDecision.packedMoveIndex & MOVE_INDEX_MASK; if (moveIndex == SWITCH_MOVE_INDEX) { @@ -64,11 +78,18 @@ contract ZapStatus is StatusEffect { return (bytes32(uint256(ALREADY_SKIPPED)), false); } - function onRemove(bytes32 data, uint256 targetIndex, uint256 monIndex) public override { - super.onRemove(data, targetIndex, monIndex); + function onRemove( + bytes32 battleKey, + bytes32 data, + uint256 targetIndex, + uint256 monIndex, + uint256 p0ActiveMonIndex, + uint256 p1ActiveMonIndex + ) public override { + super.onRemove(battleKey, data, targetIndex, monIndex, p0ActiveMonIndex, p1ActiveMonIndex); } - function onRoundEnd(uint256, bytes32 extraData, uint256, uint256) + function onRoundEnd(bytes32, uint256, bytes32 extraData, uint256, uint256, uint256, uint256) public pure override @@ -79,4 +100,4 @@ contract ZapStatus is StatusEffect { // Otherwise keep the effect return (extraData, state == ALREADY_SKIPPED); } -} \ No newline at end of file +} diff --git a/src/lib/ValidatorLogic.sol b/src/lib/ValidatorLogic.sol new file mode 100644 index 00000000..2f57c320 --- /dev/null +++ b/src/lib/ValidatorLogic.sol @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity ^0.8.0; + +import "../Constants.sol"; +import "../moves/IMoveSet.sol"; + +/// @title ValidatorLogic +/// @notice Pure validation logic extracted from DefaultValidator for reuse by Engine +/// @dev This library contains no external calls - all data must be passed in +library ValidatorLogic { + /// @notice Validates a specific move selection (stamina + move's own validation) + /// @param battleKey The battle identifier + /// @param moveSet The move being used + /// @param playerIndex The player using the move + /// @param activeMonIndex The active mon index for this player + /// @param extraData Extra data for the move + /// @param baseStamina The mon's base stamina + /// @param staminaDelta The mon's current stamina delta (or CLEARED_MON_STATE_SENTINEL if unset) + /// @return valid Whether the move selection is valid + function validateSpecificMoveSelection( + bytes32 battleKey, + IMoveSet moveSet, + uint256 playerIndex, + uint256 activeMonIndex, + uint240 extraData, + uint32 baseStamina, + int32 staminaDelta + ) internal view returns (bool valid) { + // Handle sentinel value + int256 effectiveDelta = staminaDelta == CLEARED_MON_STATE_SENTINEL ? int256(0) : int256(staminaDelta); + uint256 currentStamina = uint256(int256(uint256(baseStamina)) + effectiveDelta); + + // Check stamina cost + if (moveSet.stamina(battleKey, playerIndex, activeMonIndex) > currentStamina) { + return false; + } + + // Check move's own validation + if (!moveSet.isValidTarget(battleKey, extraData)) { + return false; + } + + return true; + } + + /// @notice Validates a switch to a different mon + /// @param turnId Current turn ID + /// @param activeMonIndex The current active mon index for this player + /// @param monToSwitchIndex The mon index to switch to + /// @param isTargetKnockedOut Whether the target mon is knocked out + /// @param monsPerTeam Maximum mons per team + /// @return valid Whether the switch is valid + function validateSwitch( + uint64 turnId, + uint256 activeMonIndex, + uint256 monToSwitchIndex, + bool isTargetKnockedOut, + uint256 monsPerTeam + ) internal pure returns (bool valid) { + // Check bounds + if (monToSwitchIndex >= monsPerTeam) { + return false; + } + + // Cannot switch to a knocked out mon + if (isTargetKnockedOut) { + return false; + } + + // Cannot switch to the same mon (except on turn 0 for initial switch-in) + if (turnId != 0 && monToSwitchIndex == activeMonIndex) { + return false; + } + + return true; + } + + /// @notice Validates a player's move selection (high-level validation) + /// @dev This combines switch validation and move validation logic + /// @param moveIndex The move index (0-3 for moves, SWITCH_MOVE_INDEX for switch, NO_OP_MOVE_INDEX for no-op) + /// @param turnId Current turn ID + /// @param isActiveMonKnockedOut Whether the active mon is knocked out + /// @param movesPerMon Maximum moves per mon + /// @return requiresSwitch Whether the validation requires a switch + /// @return isNoOp Whether this is a no-op move + /// @return isSwitch Whether this is a switch move + /// @return isRegularMove Whether this is a regular move (0-3) + /// @return valid Whether basic validation passes (bounds check, forced switch check) + function validatePlayerMoveBasics( + uint256 moveIndex, + uint64 turnId, + bool isActiveMonKnockedOut, + uint256 movesPerMon + ) + internal + pure + returns (bool requiresSwitch, bool isNoOp, bool isSwitch, bool isRegularMove, bool valid) + { + // On turn 0 or if active mon is KO'd, must switch + requiresSwitch = (turnId == 0) || isActiveMonKnockedOut; + + if (requiresSwitch && moveIndex != SWITCH_MOVE_INDEX) { + return (requiresSwitch, false, false, false, false); + } + + // Identify move type + isNoOp = (moveIndex == NO_OP_MOVE_INDEX); + isSwitch = (moveIndex == SWITCH_MOVE_INDEX); + isRegularMove = !isNoOp && !isSwitch; + + // Bounds check for regular moves + if (isRegularMove && moveIndex >= movesPerMon) { + return (requiresSwitch, isNoOp, isSwitch, isRegularMove, false); + } + + return (requiresSwitch, isNoOp, isSwitch, isRegularMove, true); + } +} diff --git a/src/mons/aurox/BullRush.sol b/src/mons/aurox/BullRush.sol index decd3ef5..73b6380c 100644 --- a/src/mons/aurox/BullRush.sol +++ b/src/mons/aurox/BullRush.sol @@ -35,18 +35,19 @@ contract BullRush is StandardAttack { ) {} - function move(bytes32 battleKey, uint256 attackerPlayerIndex, uint240, uint256 rng) - public - override - { + function move( + bytes32 battleKey, + uint256 attackerPlayerIndex, + uint256 attackerMonIndex, + uint256 defenderMonIndex, + uint240, + uint256 rng + ) public override { // Deal the damage to opponent - (int32 damage,) = _move(battleKey, attackerPlayerIndex, rng); + (int32 damage,) = _move(battleKey, attackerPlayerIndex, defenderMonIndex, rng); // Deal self-damage if (damage > 0) { - uint256[] memory activeMonIndex = ENGINE.getActiveMonIndexForBattleState(battleKey); - uint256 attackerMonIndex = activeMonIndex[attackerPlayerIndex]; - int32 maxHp = int32( ENGINE.getMonValueForBattle(battleKey, attackerPlayerIndex, attackerMonIndex, MonStateIndexName.Hp) ); diff --git a/src/mons/aurox/GildedRecovery.sol b/src/mons/aurox/GildedRecovery.sol index 2f1c97d6..76b57321 100644 --- a/src/mons/aurox/GildedRecovery.sol +++ b/src/mons/aurox/GildedRecovery.sol @@ -24,7 +24,14 @@ contract GildedRecovery is IMoveSet { return "Gilded Recovery"; } - function move(bytes32 battleKey, uint256 attackerPlayerIndex, uint240 extraData, uint256) external { + function move( + bytes32 battleKey, + uint256 attackerPlayerIndex, + uint256 attackerMonIndex, + uint256, + uint240 extraData, + uint256 + ) external { // extraData contains the mon index as raw uint240 uint256 targetMonIndex = uint256(extraData); @@ -48,20 +55,19 @@ contract GildedRecovery is IMoveSet { ENGINE.updateMonState(attackerPlayerIndex, targetMonIndex, MonStateIndexName.Stamina, STAMINA_BONUS); // Heal 50% of max HP for self - uint256 activeMonIndex = ENGINE.getActiveMonIndexForBattleState(battleKey)[attackerPlayerIndex]; int32 maxHp = - int32(ENGINE.getMonValueForBattle(battleKey, attackerPlayerIndex, activeMonIndex, MonStateIndexName.Hp)); + int32(ENGINE.getMonValueForBattle(battleKey, attackerPlayerIndex, attackerMonIndex, MonStateIndexName.Hp)); int32 healAmount = (maxHp * HEAL_PERCENT) / 100; // Don't overheal int32 currentHpDelta = - ENGINE.getMonStateForBattle(battleKey, attackerPlayerIndex, activeMonIndex, MonStateIndexName.Hp); + ENGINE.getMonStateForBattle(battleKey, attackerPlayerIndex, attackerMonIndex, MonStateIndexName.Hp); if (currentHpDelta + healAmount > 0) { healAmount = -currentHpDelta; } if (healAmount != 0) { - ENGINE.updateMonState(attackerPlayerIndex, activeMonIndex, MonStateIndexName.Hp, healAmount); + ENGINE.updateMonState(attackerPlayerIndex, attackerMonIndex, MonStateIndexName.Hp, healAmount); } } // If no status effect, do nothing diff --git a/src/mons/aurox/IronWall.sol b/src/mons/aurox/IronWall.sol index ada3f3b9..e54f7fe4 100644 --- a/src/mons/aurox/IronWall.sol +++ b/src/mons/aurox/IronWall.sol @@ -11,7 +11,7 @@ import {IEffect} from "../../effects/IEffect.sol"; import {IMoveSet} from "../../moves/IMoveSet.sol"; contract IronWall is IMoveSet, BasicEffect { - + int32 public constant HEAL_PERCENT = 50; int32 public constant INITIAL_HEAL_PERCENT = 20; @@ -25,12 +25,16 @@ contract IronWall is IMoveSet, BasicEffect { return "Iron Wall"; } - function move(bytes32 battleKey, uint256 attackerPlayerIndex, uint240, uint256) external { - // Get the active mon index - uint256 activeMonIndex = ENGINE.getActiveMonIndexForBattleState(battleKey)[attackerPlayerIndex]; - + function move( + bytes32 battleKey, + uint256 attackerPlayerIndex, + uint256 attackerMonIndex, + uint256, + uint240, + uint256 + ) external { // Check to see if the effect is already active - (EffectInstance[] memory effects, ) = ENGINE.getEffects(battleKey, attackerPlayerIndex, activeMonIndex); + (EffectInstance[] memory effects, ) = ENGINE.getEffects(battleKey, attackerPlayerIndex, attackerMonIndex); for (uint256 i = 0; i < effects.length; i++) { if (address(effects[i].effect) == address(this)) { return; @@ -38,19 +42,19 @@ contract IronWall is IMoveSet, BasicEffect { } // The effect will last until Aurox switches out - ENGINE.addEffect(attackerPlayerIndex, activeMonIndex, IEffect(address(this)), bytes32(0)); + ENGINE.addEffect(attackerPlayerIndex, attackerMonIndex, IEffect(address(this)), bytes32(0)); // Also, heal for INITIAL_HEAL_PERCENT int32 maxHp = - int32(ENGINE.getMonValueForBattle(battleKey, attackerPlayerIndex, activeMonIndex, MonStateIndexName.Hp)); + int32(ENGINE.getMonValueForBattle(battleKey, attackerPlayerIndex, attackerMonIndex, MonStateIndexName.Hp)); int32 healAmount = (maxHp * INITIAL_HEAL_PERCENT) / 100; // Prevent overhealing int32 currentHpDelta = - ENGINE.getMonStateForBattle(battleKey, attackerPlayerIndex, activeMonIndex, MonStateIndexName.Hp); + ENGINE.getMonStateForBattle(battleKey, attackerPlayerIndex, attackerMonIndex, MonStateIndexName.Hp); if (currentHpDelta + healAmount > 0) { healAmount = -currentHpDelta; } - ENGINE.updateMonState(attackerPlayerIndex, activeMonIndex, MonStateIndexName.Hp, healAmount); + ENGINE.updateMonState(attackerPlayerIndex, attackerMonIndex, MonStateIndexName.Hp, healAmount); } function stamina(bytes32, uint256, uint256) external pure returns (uint32) { @@ -83,7 +87,7 @@ contract IronWall is IMoveSet, BasicEffect { return 0x60; } - function onAfterDamage(uint256, bytes32 extraData, uint256 targetIndex, uint256 monIndex, int32 damageDealt) + function onAfterDamage(bytes32 battleKey, uint256, bytes32 extraData, uint256 targetIndex, uint256 monIndex, uint256, uint256, int32 damageDealt) external override returns (bytes32 updatedExtraData, bool removeAfterRun) @@ -94,7 +98,7 @@ contract IronWall is IMoveSet, BasicEffect { if ( healAmount > 0 && ENGINE.getMonStateForBattle( - ENGINE.battleKeyForWrite(), targetIndex, monIndex, MonStateIndexName.IsKnockedOut + battleKey, targetIndex, monIndex, MonStateIndexName.IsKnockedOut ) == 0 ) { ENGINE.updateMonState(targetIndex, monIndex, MonStateIndexName.Hp, healAmount); @@ -102,7 +106,7 @@ contract IronWall is IMoveSet, BasicEffect { return (extraData, false); } - function onMonSwitchOut(uint256, bytes32, uint256, uint256) + function onMonSwitchOut(bytes32, uint256, bytes32, uint256, uint256, uint256, uint256) external pure override @@ -110,4 +114,4 @@ contract IronWall is IMoveSet, BasicEffect { { return (bytes32(0), true); } -} \ No newline at end of file +} diff --git a/src/mons/aurox/UpOnly.sol b/src/mons/aurox/UpOnly.sol index 112d1682..a9180238 100644 --- a/src/mons/aurox/UpOnly.sol +++ b/src/mons/aurox/UpOnly.sol @@ -45,7 +45,7 @@ contract UpOnly is IAbility, BasicEffect { return 0x40; } - function onAfterDamage(uint256, bytes32 extraData, uint256 targetIndex, uint256 monIndex, int32) + function onAfterDamage(bytes32, uint256, bytes32 extraData, uint256 targetIndex, uint256 monIndex, uint256, uint256, int32) external override returns (bytes32 updatedExtraData, bool removeAfterRun) @@ -61,4 +61,4 @@ contract UpOnly is IAbility, BasicEffect { return (extraData, false); } -} \ No newline at end of file +} diff --git a/src/mons/aurox/VolatilePunch.sol b/src/mons/aurox/VolatilePunch.sol index 570b48e2..58659cca 100644 --- a/src/mons/aurox/VolatilePunch.sol +++ b/src/mons/aurox/VolatilePunch.sol @@ -41,18 +41,20 @@ contract VolatilePunch is StandardAttack { FROSTBITE_STATUS = _FROSTBITE_STATUS; } - function move(bytes32 battleKey, uint256 attackerPlayerIndex, uint240, uint256 rng) - public - override - { + function move( + bytes32 battleKey, + uint256 attackerPlayerIndex, + uint256, + uint256 defenderMonIndex, + uint240, + uint256 rng + ) public override { // Deal the damage to opponent - (int32 damage,) = _move(battleKey, attackerPlayerIndex, rng); + (int32 damage,) = _move(battleKey, attackerPlayerIndex, defenderMonIndex, rng); // Apply status effects if damage was dealt if (damage > 0) { uint256 defenderPlayerIndex = (attackerPlayerIndex + 1) % 2; - uint256 defenderMonIndex = - ENGINE.getActiveMonIndexForBattleState(ENGINE.battleKeyForWrite())[defenderPlayerIndex]; // Use a different part of the RNG for status application uint256 statusRng = uint256(keccak256(abi.encode(rng, "STATUS_EFFECT"))); diff --git a/src/mons/ekineki/BubbleBop.sol b/src/mons/ekineki/BubbleBop.sol index be8f7535..65401e3b 100644 --- a/src/mons/ekineki/BubbleBop.sol +++ b/src/mons/ekineki/BubbleBop.sol @@ -35,7 +35,14 @@ contract BubbleBop is StandardAttack { ) {} - function move(bytes32 battleKey, uint256 attackerPlayerIndex, uint240, uint256 rng) public override { + function move( + bytes32 battleKey, + uint256 attackerPlayerIndex, + uint256, + uint256, + uint240, + uint256 rng + ) public override { uint32 effectiveCritRate = NineNineNineLib._getEffectiveCritRate(ENGINE, battleKey, attackerPlayerIndex); // First hit diff --git a/src/mons/ekineki/NineNineNine.sol b/src/mons/ekineki/NineNineNine.sol index c107b916..2c244691 100644 --- a/src/mons/ekineki/NineNineNine.sol +++ b/src/mons/ekineki/NineNineNine.sol @@ -20,7 +20,7 @@ contract NineNineNine is IMoveSet { return "Nine Nine Nine"; } - function move(bytes32 battleKey, uint256 attackerPlayerIndex, uint240, uint256) external { + function move(bytes32 battleKey, uint256 attackerPlayerIndex, uint256, uint256, uint240, uint256) external { // Set crit boost for the next turn uint256 currentTurn = ENGINE.getTurnIdForBattleState(battleKey); bytes32 key = NineNineNineLib._getKey(attackerPlayerIndex); diff --git a/src/mons/ekineki/Overflow.sol b/src/mons/ekineki/Overflow.sol index b204ade1..eabc4983 100644 --- a/src/mons/ekineki/Overflow.sol +++ b/src/mons/ekineki/Overflow.sol @@ -35,7 +35,14 @@ contract Overflow is StandardAttack { ) {} - function move(bytes32 battleKey, uint256 attackerPlayerIndex, uint240, uint256 rng) public override { + function move( + bytes32 battleKey, + uint256 attackerPlayerIndex, + uint256, + uint256, + uint240, + uint256 rng + ) public override { uint32 effectiveCritRate = NineNineNineLib._getEffectiveCritRate(ENGINE, battleKey, attackerPlayerIndex); AttackCalculator._calculateDamage( ENGINE, diff --git a/src/mons/ekineki/SneakAttack.sol b/src/mons/ekineki/SneakAttack.sol index b09cd6bc..d5c480e8 100644 --- a/src/mons/ekineki/SneakAttack.sol +++ b/src/mons/ekineki/SneakAttack.sol @@ -30,9 +30,15 @@ contract SneakAttack is IMoveSet, BasicEffect { return "Sneak Attack"; } - function move(bytes32 battleKey, uint256 attackerPlayerIndex, uint240 extraData, uint256 rng) external { + function move( + bytes32 battleKey, + uint256 attackerPlayerIndex, + uint256 attackerMonIndex, + uint256, + uint240 extraData, + uint256 rng + ) external { // Check if already used this switch-in (effect present = already used) - uint256 attackerMonIndex = ENGINE.getActiveMonIndexForBattleState(battleKey)[attackerPlayerIndex]; (EffectInstance[] memory effects,) = ENGINE.getEffects(battleKey, attackerPlayerIndex, attackerMonIndex); for (uint256 i = 0; i < effects.length; i++) { if (address(effects[i].effect) == address(this)) { @@ -118,7 +124,7 @@ contract SneakAttack is IMoveSet, BasicEffect { return 0x20; } - function onMonSwitchOut(uint256, bytes32, uint256, uint256) + function onMonSwitchOut(bytes32, uint256, bytes32, uint256, uint256, uint256, uint256) external pure override diff --git a/src/mons/embursa/HeatBeacon.sol b/src/mons/embursa/HeatBeacon.sol index b03e2d2c..98bcf995 100644 --- a/src/mons/embursa/HeatBeacon.sol +++ b/src/mons/embursa/HeatBeacon.sol @@ -23,11 +23,16 @@ contract HeatBeacon is IMoveSet { return "Heat Beacon"; } - function move(bytes32, uint256 attackerPlayerIndex, uint240, uint256) external { + function move( + bytes32, + uint256 attackerPlayerIndex, + uint256, + uint256 defenderMonIndex, + uint240, + uint256 + ) external { // Apply burn to opposing mon uint256 defenderPlayerIndex = (attackerPlayerIndex + 1) % 2; - uint256 defenderMonIndex = - ENGINE.getActiveMonIndexForBattleState(ENGINE.battleKeyForWrite())[defenderPlayerIndex]; ENGINE.addEffect(defenderPlayerIndex, defenderMonIndex, BURN_STATUS, ""); // Clear the priority boost diff --git a/src/mons/embursa/HoneyBribe.sol b/src/mons/embursa/HoneyBribe.sol index fea16b55..73754278 100644 --- a/src/mons/embursa/HoneyBribe.sol +++ b/src/mons/embursa/HoneyBribe.sol @@ -39,22 +39,27 @@ contract HoneyBribe is IMoveSet { } } - function move(bytes32 battleKey, uint256 attackerPlayerIndex, uint240, uint256) external { + function move( + bytes32 battleKey, + uint256 attackerPlayerIndex, + uint256 attackerMonIndex, + uint256 defenderMonIndex, + uint240, + uint256 + ) external { // Heal active mon by max HP / 2**bribeLevel - uint256 activeMonIndex = ENGINE.getActiveMonIndexForBattleState(battleKey)[attackerPlayerIndex]; - uint256 bribeLevel = _getBribeLevel(battleKey, attackerPlayerIndex, activeMonIndex); - uint32 maxHp = ENGINE.getMonValueForBattle(battleKey, attackerPlayerIndex, activeMonIndex, MonStateIndexName.Hp); + uint256 bribeLevel = _getBribeLevel(battleKey, attackerPlayerIndex, attackerMonIndex); + uint32 maxHp = ENGINE.getMonValueForBattle(battleKey, attackerPlayerIndex, attackerMonIndex, MonStateIndexName.Hp); int32 healAmount = int32(uint32(maxHp / (DEFAULT_HEAL_DENOM * (2 ** bribeLevel)))); int32 currentDamage = - ENGINE.getMonStateForBattle(battleKey, attackerPlayerIndex, activeMonIndex, MonStateIndexName.Hp); + ENGINE.getMonStateForBattle(battleKey, attackerPlayerIndex, attackerMonIndex, MonStateIndexName.Hp); if (currentDamage + healAmount > 0) { healAmount = -1 * currentDamage; } - ENGINE.updateMonState(attackerPlayerIndex, activeMonIndex, MonStateIndexName.Hp, healAmount); + ENGINE.updateMonState(attackerPlayerIndex, attackerMonIndex, MonStateIndexName.Hp, healAmount); // Heal opposing active mon by max HP / 2**(bribeLevel + 1) uint256 defenderPlayerIndex = (attackerPlayerIndex + 1) % 2; - uint256 defenderMonIndex = ENGINE.getActiveMonIndexForBattleState(battleKey)[defenderPlayerIndex]; healAmount = int32(uint32(maxHp / (DEFAULT_HEAL_DENOM * (2 ** (bribeLevel + 1))))); currentDamage = ENGINE.getMonStateForBattle(battleKey, defenderPlayerIndex, defenderMonIndex, MonStateIndexName.Hp); @@ -73,7 +78,7 @@ contract HoneyBribe is IMoveSet { STAT_BOOSTS.addStatBoosts(defenderPlayerIndex, defenderMonIndex, statBoosts, StatBoostFlag.Temp); // Update the bribe level - _increaseBribeLevel(battleKey, attackerPlayerIndex, activeMonIndex); + _increaseBribeLevel(battleKey, attackerPlayerIndex, attackerMonIndex); // Clear the priority boost if (HeatBeaconLib._getPriorityBoost(ENGINE, attackerPlayerIndex) == 1) { diff --git a/src/mons/embursa/Q5.sol b/src/mons/embursa/Q5.sol index 3cc9b0b8..7542f2cd 100644 --- a/src/mons/embursa/Q5.sol +++ b/src/mons/embursa/Q5.sol @@ -37,7 +37,7 @@ contract Q5 is IMoveSet, BasicEffect { attackerPlayerIndex = uint256(data) & type(uint128).max; } - function move(bytes32, uint256 attackerPlayerIndex, uint240, uint256) external { + function move(bytes32, uint256 attackerPlayerIndex, uint256, uint256, uint240, uint256) external { // Add effect to global effects ENGINE.addEffect(2, attackerPlayerIndex, this, _packExtraData(1, attackerPlayerIndex)); @@ -77,7 +77,7 @@ contract Q5 is IMoveSet, BasicEffect { return 0x02; } - function onRoundStart(uint256 rng, bytes32 extraData, uint256, uint256) + function onRoundStart(bytes32 battleKey, uint256 rng, bytes32 extraData, uint256, uint256, uint256, uint256) external override returns (bytes32, bool) @@ -88,13 +88,13 @@ contract Q5 is IMoveSet, BasicEffect { AttackCalculator._calculateDamage( ENGINE, TYPE_CALCULATOR, - ENGINE.battleKeyForWrite(), + battleKey, attackerPlayerIndex, BASE_POWER, DEFAULT_ACCURACY, DEFAULT_VOL, - moveType(ENGINE.battleKeyForWrite()), - moveClass(ENGINE.battleKeyForWrite()), + moveType(battleKey), + moveClass(battleKey), rng, DEFAULT_CRIT_RATE ); diff --git a/src/mons/embursa/SetAblaze.sol b/src/mons/embursa/SetAblaze.sol index 7576eb3a..23348bb4 100644 --- a/src/mons/embursa/SetAblaze.sol +++ b/src/mons/embursa/SetAblaze.sol @@ -35,8 +35,15 @@ contract SetAblaze is StandardAttack { ) {} - function move(bytes32 battleKey, uint256 attackerPlayerIndex, uint240 args, uint256 rng) public override { - super.move(battleKey, attackerPlayerIndex, args, rng); + function move( + bytes32 battleKey, + uint256 attackerPlayerIndex, + uint256 attackerMonIndex, + uint256 defenderMonIndex, + uint240 args, + uint256 rng + ) public override { + super.move(battleKey, attackerPlayerIndex, attackerMonIndex, defenderMonIndex, args, rng); // Clear the priority boost if (HeatBeaconLib._getPriorityBoost(ENGINE, attackerPlayerIndex) == 1) { HeatBeaconLib._clearPriorityBoost(ENGINE, attackerPlayerIndex); diff --git a/src/mons/embursa/Tinderclaws.sol b/src/mons/embursa/Tinderclaws.sol index b4414848..fe37d787 100644 --- a/src/mons/embursa/Tinderclaws.sol +++ b/src/mons/embursa/Tinderclaws.sol @@ -46,12 +46,11 @@ contract Tinderclaws is IAbility, BasicEffect { } // extraData: 0 = no SpATK boost applied, 1 = SpATK boost applied - function onAfterMove(uint256 rng, bytes32 extraData, uint256 targetIndex, uint256 monIndex) + function onAfterMove(bytes32 battleKey, uint256 rng, bytes32 extraData, uint256 targetIndex, uint256 monIndex, uint256, uint256) external override returns (bytes32 updatedExtraData, bool removeAfterRun) { - bytes32 battleKey = ENGINE.battleKeyForWrite(); MoveDecision memory moveDecision = ENGINE.getMoveDecisionForBattleState(battleKey, targetIndex); // Unpack the move index from packedMoveIndex uint8 moveIndex = moveDecision.packedMoveIndex & MOVE_INDEX_MASK; @@ -66,7 +65,7 @@ contract Tinderclaws is IAbility, BasicEffect { rng = uint256(keccak256(abi.encode(rng, targetIndex, monIndex, address(this)))); if (rng % BURN_CHANCE == BURN_CHANCE - 1) { // Apply burn to self (if it can be applied) - if (BURN_STATUS.shouldApply(battleKey, targetIndex, monIndex)) { + if (BURN_STATUS.shouldApply(battleKey, bytes32(0), targetIndex, monIndex)) { ENGINE.addEffect(targetIndex, monIndex, BURN_STATUS, bytes32(0)); } } @@ -75,12 +74,11 @@ contract Tinderclaws is IAbility, BasicEffect { return (extraData, false); } - function onRoundEnd(uint256, bytes32 extraData, uint256 targetIndex, uint256 monIndex) + function onRoundEnd(bytes32 battleKey, uint256, bytes32 extraData, uint256 targetIndex, uint256 monIndex, uint256, uint256) external override returns (bytes32 updatedExtraData, bool removeAfterRun) { - bytes32 battleKey = ENGINE.battleKeyForWrite(); bool isBurned = _isBurned(battleKey, targetIndex, monIndex); bool hasBoost = uint256(extraData) == 1; @@ -120,4 +118,3 @@ contract Tinderclaws is IAbility, BasicEffect { } } } - diff --git a/src/mons/ghouliath/EternalGrudge.sol b/src/mons/ghouliath/EternalGrudge.sol index 75a1cbcb..016c3b6f 100644 --- a/src/mons/ghouliath/EternalGrudge.sol +++ b/src/mons/ghouliath/EternalGrudge.sol @@ -26,10 +26,16 @@ contract EternalGrudge is IMoveSet { return "Eternal Grudge"; } - function move(bytes32 battleKey, uint256 attackerPlayerIndex, uint240, uint256) external { + function move( + bytes32 battleKey, + uint256 attackerPlayerIndex, + uint256 attackerMonIndex, + uint256 defenderMonIndex, + uint240, + uint256 + ) external { // Apply the debuff (50% debuff to both attack and special attack) uint256 defenderPlayerIndex = (attackerPlayerIndex + 1) % 2; - uint256 defenderMonIndex = ENGINE.getActiveMonIndexForBattleState(battleKey)[defenderPlayerIndex]; StatBoostToApply[] memory statBoosts = new StatBoostToApply[](2); statBoosts[0] = StatBoostToApply({ stat: MonStateIndexName.Attack, @@ -42,7 +48,6 @@ contract EternalGrudge is IMoveSet { boostType: StatBoostType.Divide }); STAT_BOOSTS.addStatBoosts(defenderPlayerIndex, defenderMonIndex, statBoosts, StatBoostFlag.Temp); - uint256 attackerMonIndex = ENGINE.getActiveMonIndexForBattleState(battleKey)[attackerPlayerIndex]; // KO self by dealing just enough damage int32 currentDamage = diff --git a/src/mons/ghouliath/RiseFromTheGrave.sol b/src/mons/ghouliath/RiseFromTheGrave.sol index cbaaea4d..658152fc 100644 --- a/src/mons/ghouliath/RiseFromTheGrave.sol +++ b/src/mons/ghouliath/RiseFromTheGrave.sol @@ -48,7 +48,7 @@ contract RiseFromTheGrave is IAbility, BasicEffect { return 0x44; } - function onAfterDamage(uint256, bytes32 extraData, uint256 targetIndex, uint256 monIndex, int32) + function onAfterDamage(bytes32 battleKey, uint256, bytes32 extraData, uint256 targetIndex, uint256 monIndex, uint256, uint256, int32) external override returns (bytes32 updatedExtraData, bool removeAfterRun) @@ -60,7 +60,7 @@ contract RiseFromTheGrave is IAbility, BasicEffect { // If the mon is KO'd, add this effect to the global effects list and remove the mon effect if ( ENGINE.getMonStateForBattle( - ENGINE.battleKeyForWrite(), targetIndex, monIndex, MonStateIndexName.IsKnockedOut + battleKey, targetIndex, monIndex, MonStateIndexName.IsKnockedOut ) == 1 ) { uint64 v1 = REVIVAL_DELAY; @@ -74,7 +74,7 @@ contract RiseFromTheGrave is IAbility, BasicEffect { } // Regain stamina on round end, this can overheal stamina - function onRoundEnd(uint256, bytes32 extraData, uint256, uint256) + function onRoundEnd(bytes32 battleKey, uint256, bytes32 extraData, uint256, uint256, uint256, uint256) external override returns (bytes32 updatedExtraData, bool removeAfterRun) @@ -93,7 +93,6 @@ contract RiseFromTheGrave is IAbility, BasicEffect { else if (turnsLeft == 1) { // Revive the mon and set HP to 1 ENGINE.updateMonState(playerIndex, monIndex, MonStateIndexName.IsKnockedOut, 0); - bytes32 battleKey = ENGINE.battleKeyForWrite(); int32 currentDamage = ENGINE.getMonStateForBattle(battleKey, playerIndex, monIndex, MonStateIndexName.Hp); uint32 maxHp = ENGINE.getMonValueForBattle(battleKey, playerIndex, monIndex, MonStateIndexName.Hp); int32 hpShiftAmount = 1 - currentDamage - int32(maxHp); diff --git a/src/mons/ghouliath/WitherAway.sol b/src/mons/ghouliath/WitherAway.sol index b3359cd8..2247cec3 100644 --- a/src/mons/ghouliath/WitherAway.sol +++ b/src/mons/ghouliath/WitherAway.sol @@ -34,15 +34,18 @@ contract WitherAway is StandardAttack { ) {} - function move(bytes32 battleKey, uint256 attackerPlayerIndex, uint240 extraData, uint256 rng) - public - override - { + function move( + bytes32 battleKey, + uint256 attackerPlayerIndex, + uint256 attackerMonIndex, + uint256 defenderMonIndex, + uint240 extraData, + uint256 rng + ) public override { // Deal the damage and inflict panic - super.move(battleKey, attackerPlayerIndex, extraData, rng); + super.move(battleKey, attackerPlayerIndex, attackerMonIndex, defenderMonIndex, extraData, rng); // Also inflict panic on self - uint256 activeMonIndex = ENGINE.getActiveMonIndexForBattleState(battleKey)[attackerPlayerIndex]; - ENGINE.addEffect(attackerPlayerIndex, activeMonIndex, effect(battleKey), ""); + ENGINE.addEffect(attackerPlayerIndex, attackerMonIndex, effect(battleKey), ""); } } diff --git a/src/mons/gorillax/Angery.sol b/src/mons/gorillax/Angery.sol index dcb28470..e5c6e3fb 100644 --- a/src/mons/gorillax/Angery.sol +++ b/src/mons/gorillax/Angery.sol @@ -44,7 +44,7 @@ contract Angery is IAbility, BasicEffect { return 0x44; } - function onRoundEnd(uint256, bytes32 extraData, uint256 targetIndex, uint256 monIndex) + function onRoundEnd(bytes32 battleKey, uint256, bytes32 extraData, uint256 targetIndex, uint256 monIndex, uint256, uint256) external override returns (bytes32 updatedExtraData, bool removeAfterRun) @@ -54,7 +54,7 @@ contract Angery is IAbility, BasicEffect { // Heal int32 healAmount = int32( - ENGINE.getMonValueForBattle(ENGINE.battleKeyForWrite(), targetIndex, monIndex, MonStateIndexName.Hp) + ENGINE.getMonValueForBattle(battleKey, targetIndex, monIndex, MonStateIndexName.Hp) ) / MAX_HP_DENOM; ENGINE.updateMonState(targetIndex, monIndex, MonStateIndexName.Hp, healAmount); // Reset the charges @@ -64,7 +64,7 @@ contract Angery is IAbility, BasicEffect { } } - function onAfterDamage(uint256, bytes32 extraData, uint256, uint256, int32) + function onAfterDamage(bytes32, uint256, bytes32 extraData, uint256, uint256, uint256, uint256, int32) external pure override diff --git a/src/mons/gorillax/RockPull.sol b/src/mons/gorillax/RockPull.sol index a864edf4..77072089 100644 --- a/src/mons/gorillax/RockPull.sol +++ b/src/mons/gorillax/RockPull.sol @@ -36,7 +36,14 @@ contract RockPull is IMoveSet { return moveIndex == SWITCH_MOVE_INDEX; } - function move(bytes32 battleKey, uint256 attackerPlayerIndex, uint240, uint256 rng) external { + function move( + bytes32 battleKey, + uint256 attackerPlayerIndex, + uint256 attackerMonIndex, + uint256, + uint240, + uint256 rng + ) external { if (_didOtherPlayerChooseSwitch(battleKey, attackerPlayerIndex)) { // Deal damage to the opposing mon AttackCalculator._calculateDamage( @@ -68,8 +75,7 @@ contract RockPull is IMoveSet { rng, DEFAULT_CRIT_RATE ); - uint256[] memory monIndex = ENGINE.getActiveMonIndexForBattleState(battleKey); - ENGINE.dealDamage(attackerPlayerIndex, monIndex[attackerPlayerIndex], selfDamage); + ENGINE.dealDamage(attackerPlayerIndex, attackerMonIndex, selfDamage); } } diff --git a/src/mons/iblivion/Baselight.sol b/src/mons/iblivion/Baselight.sol index 22048d84..05346d85 100644 --- a/src/mons/iblivion/Baselight.sol +++ b/src/mons/iblivion/Baselight.sol @@ -52,19 +52,17 @@ contract Baselight is IAbility, BasicEffect { return level; } - function setBaselightLevel(uint256 playerIndex, uint256 monIndex, uint256 level) public { + function setBaselightLevel(bytes32 battleKey, uint256 playerIndex, uint256 monIndex, uint256 level) public { if (level > MAX_BASELIGHT_LEVEL) { level = MAX_BASELIGHT_LEVEL; } - bytes32 battleKey = ENGINE.battleKeyForWrite(); (bool exists, uint256 effectIndex,) = _findBaselightEffect(battleKey, playerIndex, monIndex); if (exists) { ENGINE.editEffect(playerIndex, monIndex, effectIndex, bytes32(level)); } } - function decreaseBaselightLevel(uint256 playerIndex, uint256 monIndex, uint256 amount) public { - bytes32 battleKey = ENGINE.battleKeyForWrite(); + function decreaseBaselightLevel(bytes32 battleKey, uint256 playerIndex, uint256 monIndex, uint256 amount) public { (bool exists, uint256 effectIndex, uint256 currentLevel) = _findBaselightEffect(battleKey, playerIndex, monIndex); if (exists) { uint256 newLevel = amount >= currentLevel ? 0 : currentLevel - amount; @@ -87,7 +85,7 @@ contract Baselight is IAbility, BasicEffect { return 0x04; } - function onRoundEnd(uint256, bytes32 extraData, uint256, uint256) + function onRoundEnd(bytes32, uint256, bytes32 extraData, uint256, uint256, uint256, uint256) external pure override diff --git a/src/mons/iblivion/Brightback.sol b/src/mons/iblivion/Brightback.sol index 5cf9ee58..6ada1ef5 100644 --- a/src/mons/iblivion/Brightback.sol +++ b/src/mons/iblivion/Brightback.sol @@ -36,7 +36,14 @@ contract Brightback is IMoveSet { return "Brightback"; } - function move(bytes32 battleKey, uint256 attackerPlayerIndex, uint240, uint256 rng) external { + function move( + bytes32 battleKey, + uint256 attackerPlayerIndex, + uint256 attackerMonIndex, + uint256, + uint240, + uint256 rng + ) external { (int32 damageDealt,) = AttackCalculator._calculateDamage( ENGINE, TYPE_CALCULATOR, @@ -51,17 +58,16 @@ contract Brightback is IMoveSet { DEFAULT_CRIT_RATE ); - uint256 monIndex = ENGINE.getActiveMonIndexForBattleState(battleKey)[attackerPlayerIndex]; - uint256 baselightLevel = BASELIGHT.getBaselightLevel(battleKey, attackerPlayerIndex, monIndex); + uint256 baselightLevel = BASELIGHT.getBaselightLevel(battleKey, attackerPlayerIndex, attackerMonIndex); // Only heal if we have at least 1 Baselight stack if (baselightLevel >= 1) { // Consume 1 Baselight stack - BASELIGHT.decreaseBaselightLevel(attackerPlayerIndex, monIndex, 1); + BASELIGHT.decreaseBaselightLevel(battleKey, attackerPlayerIndex, attackerMonIndex, 1); // Heal for half of damage done int32 healAmount = damageDealt / 2; - int32 hpDelta = ENGINE.getMonStateForBattle(battleKey, attackerPlayerIndex, monIndex, MonStateIndexName.Hp); + int32 hpDelta = ENGINE.getMonStateForBattle(battleKey, attackerPlayerIndex, attackerMonIndex, MonStateIndexName.Hp); // Prevent overhealing if (hpDelta + healAmount > 0) { @@ -69,7 +75,7 @@ contract Brightback is IMoveSet { } // Do the heal - ENGINE.updateMonState(attackerPlayerIndex, monIndex, MonStateIndexName.Hp, healAmount); + ENGINE.updateMonState(attackerPlayerIndex, attackerMonIndex, MonStateIndexName.Hp, healAmount); } } diff --git a/src/mons/iblivion/Loop.sol b/src/mons/iblivion/Loop.sol index 6602a77f..a9fa86b2 100644 --- a/src/mons/iblivion/Loop.sol +++ b/src/mons/iblivion/Loop.sol @@ -65,16 +65,21 @@ contract Loop is IMoveSet { } } - function move(bytes32 battleKey, uint256 attackerPlayerIndex, uint240, uint256) external { - uint256 monIndex = ENGINE.getActiveMonIndexForBattleState(battleKey)[attackerPlayerIndex]; - + function move( + bytes32 battleKey, + uint256 attackerPlayerIndex, + uint256 attackerMonIndex, + uint256, + uint240, + uint256 + ) external { // Check if Loop is already active - if (isLoopActive(battleKey, attackerPlayerIndex, monIndex)) { + if (isLoopActive(battleKey, attackerPlayerIndex, attackerMonIndex)) { // Fail - Loop is already active return; } - uint256 baselightLevel = BASELIGHT.getBaselightLevel(battleKey, attackerPlayerIndex, monIndex); + uint256 baselightLevel = BASELIGHT.getBaselightLevel(battleKey, attackerPlayerIndex, attackerMonIndex); uint8 boostPercent = _getBoostPercent(baselightLevel); // If baselight level is 0, no boost to apply @@ -83,7 +88,7 @@ contract Loop is IMoveSet { } // Mark Loop as active - ENGINE.setGlobalKV(_loopActiveKey(attackerPlayerIndex, monIndex), 1); + ENGINE.setGlobalKV(_loopActiveKey(attackerPlayerIndex, attackerMonIndex), 1); // Apply stat boosts to all 5 stats (Attack, Defense, SpecialAttack, SpecialDefense, Speed) StatBoostToApply[] memory statBoosts = new StatBoostToApply[](5); @@ -114,7 +119,7 @@ contract Loop is IMoveSet { }); // Use Temp flag so boosts are removed on switch out - STAT_BOOSTS.addStatBoosts(attackerPlayerIndex, monIndex, statBoosts, StatBoostFlag.Temp); + STAT_BOOSTS.addStatBoosts(attackerPlayerIndex, attackerMonIndex, statBoosts, StatBoostFlag.Temp); } function stamina(bytes32, uint256, uint256) external pure returns (uint32) { diff --git a/src/mons/iblivion/Renormalize.sol b/src/mons/iblivion/Renormalize.sol index 75fdeb9e..3deb808a 100644 --- a/src/mons/iblivion/Renormalize.sol +++ b/src/mons/iblivion/Renormalize.sol @@ -37,17 +37,22 @@ contract Renormalize is IMoveSet { return "Renormalize"; } - function move(bytes32 battleKey, uint256 attackerPlayerIndex, uint240, uint256) external { - uint256 monIndex = ENGINE.getActiveMonIndexForBattleState(battleKey)[attackerPlayerIndex]; - + function move( + bytes32 battleKey, + uint256 attackerPlayerIndex, + uint256 attackerMonIndex, + uint256, + uint240, + uint256 + ) external { // Set Baselight level to 3 - BASELIGHT.setBaselightLevel(attackerPlayerIndex, monIndex, 3); + BASELIGHT.setBaselightLevel(battleKey, attackerPlayerIndex, attackerMonIndex, 3); // Clear Loop active flag so Loop can be used again - LOOP.clearLoopActive(attackerPlayerIndex, monIndex); + LOOP.clearLoopActive(attackerPlayerIndex, attackerMonIndex); // Clear all StatBoost effects and reset stats to base values - STAT_BOOSTS.clearAllBoostsForMon(attackerPlayerIndex, monIndex); + STAT_BOOSTS.clearAllBoostsForMon(attackerPlayerIndex, attackerMonIndex); } function stamina(bytes32, uint256, uint256) external pure returns (uint32) { diff --git a/src/mons/iblivion/UnboundedStrike.sol b/src/mons/iblivion/UnboundedStrike.sol index 02b3099b..9ac4c57f 100644 --- a/src/mons/iblivion/UnboundedStrike.sol +++ b/src/mons/iblivion/UnboundedStrike.sol @@ -40,15 +40,21 @@ contract UnboundedStrike is IMoveSet { return "Unbounded Strike"; } - function move(bytes32 battleKey, uint256 attackerPlayerIndex, uint240, uint256 rng) external { - uint256 monIndex = ENGINE.getActiveMonIndexForBattleState(battleKey)[attackerPlayerIndex]; - uint256 baselightLevel = BASELIGHT.getBaselightLevel(battleKey, attackerPlayerIndex, monIndex); + function move( + bytes32 battleKey, + uint256 attackerPlayerIndex, + uint256 attackerMonIndex, + uint256, + uint240, + uint256 rng + ) external { + uint256 baselightLevel = BASELIGHT.getBaselightLevel(battleKey, attackerPlayerIndex, attackerMonIndex); uint32 power; if (baselightLevel >= REQUIRED_STACKS) { // Empowered version: consume all 3 stacks power = EMPOWERED_POWER; - BASELIGHT.setBaselightLevel(attackerPlayerIndex, monIndex, 0); + BASELIGHT.setBaselightLevel(battleKey, attackerPlayerIndex, attackerMonIndex, 0); } else { // Normal version: no stacks consumed power = BASE_POWER; diff --git a/src/mons/inutia/ChainExpansion.sol b/src/mons/inutia/ChainExpansion.sol index 8e2a61a1..b9894faa 100644 --- a/src/mons/inutia/ChainExpansion.sol +++ b/src/mons/inutia/ChainExpansion.sol @@ -34,7 +34,7 @@ contract ChainExpansion is IMoveSet, BasicEffect { return keccak256(abi.encode(playerIndex, monIndex, name())); } - function move(bytes32 battleKey, uint256 attackerPlayerIndex, uint240, uint256) external { + function move(bytes32 battleKey, uint256 attackerPlayerIndex, uint256, uint256, uint240, uint256) external { // Check if the ability is already applied globally (EffectInstance[] memory effects, ) = ENGINE.getEffects(battleKey, 2, 2); for (uint256 i = 0; i < effects.length; i++) { @@ -83,12 +83,11 @@ contract ChainExpansion is IMoveSet, BasicEffect { playerIndex = uint256(data) & type(uint128).max; } - function onMonSwitchIn(uint256, bytes32 extraData, uint256 targetIndex, uint256 monIndex) + function onMonSwitchIn(bytes32 battleKey, uint256, bytes32 extraData, uint256 targetIndex, uint256 monIndex, uint256, uint256) external override returns (bytes32, bool) { - bytes32 battleKey = ENGINE.battleKeyForWrite(); (uint256 chargesLeft, uint256 ownerIndex) = _decodeState(extraData); // If it's a friendly mon, then we heal (flat 1/8 of max HP) if (targetIndex == ownerIndex) { diff --git a/src/mons/inutia/HitAndDip.sol b/src/mons/inutia/HitAndDip.sol index 1c675012..ed93cbe4 100644 --- a/src/mons/inutia/HitAndDip.sol +++ b/src/mons/inutia/HitAndDip.sol @@ -34,12 +34,16 @@ contract HitAndDip is StandardAttack { ) {} - function move(bytes32 battleKey, uint256 attackerPlayerIndex, uint240 extraData, uint256 rng) - public - override - { + function move( + bytes32 battleKey, + uint256 attackerPlayerIndex, + uint256, + uint256 defenderMonIndex, + uint240 extraData, + uint256 rng + ) public override { // Deal the damage - (int32 damage,) = _move(battleKey, attackerPlayerIndex, rng); + (int32 damage,) = _move(battleKey, attackerPlayerIndex, defenderMonIndex, rng); if (damage > 0) { // extraData contains the swap index as raw uint240 diff --git a/src/mons/inutia/Initialize.sol b/src/mons/inutia/Initialize.sol index ef88528d..8a09b7bf 100644 --- a/src/mons/inutia/Initialize.sol +++ b/src/mons/inutia/Initialize.sol @@ -31,18 +31,24 @@ contract Initialize is IMoveSet, BasicEffect { return keccak256(abi.encode(playerIndex, monIndex, name())); } - function move(bytes32 battleKey, uint256 attackerPlayerIndex, uint240, uint256) external { - uint256 activeMonIndex = ENGINE.getActiveMonIndexForBattleState(battleKey)[attackerPlayerIndex]; + function move( + bytes32 battleKey, + uint256 attackerPlayerIndex, + uint256 attackerMonIndex, + uint256, + uint240, + uint256 + ) external { // Check if global KV is set - uint192 flag = ENGINE.getGlobalKV(battleKey, _initializeKey(attackerPlayerIndex, activeMonIndex)); + uint192 flag = ENGINE.getGlobalKV(battleKey, _initializeKey(attackerPlayerIndex, attackerMonIndex)); if (flag == 0) { // Apply the buffs - _applyBuff(attackerPlayerIndex, activeMonIndex); + _applyBuff(attackerPlayerIndex, attackerMonIndex); // Apply effect globally - ENGINE.addEffect(2, attackerPlayerIndex, this, _encodeState(attackerPlayerIndex, activeMonIndex)); + ENGINE.addEffect(2, attackerPlayerIndex, this, _encodeState(attackerPlayerIndex, attackerMonIndex)); // Set global KV to prevent this move doing anything until Inutia swaps out - ENGINE.setGlobalKV(_initializeKey(attackerPlayerIndex, activeMonIndex), 1); + ENGINE.setGlobalKV(_initializeKey(attackerPlayerIndex, attackerMonIndex), 1); } // Otherwise we don't do anything } @@ -99,7 +105,7 @@ contract Initialize is IMoveSet, BasicEffect { monIndex = uint256(data) & type(uint128).max; } - function onMonSwitchOut(uint256, bytes32 extraData, uint256 targetIndex, uint256 monIndex) + function onMonSwitchOut(bytes32, uint256, bytes32 extraData, uint256 targetIndex, uint256 monIndex, uint256, uint256) external override returns (bytes32 updatedExtraData, bool removeAfterRun) @@ -112,7 +118,7 @@ contract Initialize is IMoveSet, BasicEffect { return (extraData, false); } - function onMonSwitchIn(uint256, bytes32 extraData, uint256 targetIndex, uint256 monIndex) + function onMonSwitchIn(bytes32, uint256, bytes32 extraData, uint256 targetIndex, uint256 monIndex, uint256, uint256) external override returns (bytes32 updatedExtraData, bool removeAfterRun) diff --git a/src/mons/inutia/Interweaving.sol b/src/mons/inutia/Interweaving.sol index e2e719b0..0d436273 100644 --- a/src/mons/inutia/Interweaving.sol +++ b/src/mons/inutia/Interweaving.sol @@ -28,7 +28,7 @@ contract Interweaving is IAbility, BasicEffect { // Lower opposing mon Attack stat uint256 otherPlayerIndex = (playerIndex + 1) % 2; uint256 otherPlayerActiveMonIndex = - ENGINE.getActiveMonIndexForBattleState(ENGINE.battleKeyForWrite())[otherPlayerIndex]; + ENGINE.getActiveMonIndexForBattleState(battleKey)[otherPlayerIndex]; StatBoostToApply[] memory statBoosts = new StatBoostToApply[](1); statBoosts[0] = StatBoostToApply({ stat: MonStateIndexName.Attack, @@ -54,14 +54,13 @@ contract Interweaving is IAbility, BasicEffect { return 0x21; } - function onMonSwitchOut(uint256, bytes32, uint256 targetIndex, uint256) + function onMonSwitchOut(bytes32, uint256, bytes32, uint256 targetIndex, uint256, uint256 p0ActiveMonIndex, uint256 p1ActiveMonIndex) external override returns (bytes32 updatedExtraData, bool removeAfterRun) { uint256 otherPlayerIndex = (targetIndex + 1) % 2; - uint256 otherPlayerActiveMonIndex = - ENGINE.getActiveMonIndexForBattleState(ENGINE.battleKeyForWrite())[otherPlayerIndex]; + uint256 otherPlayerActiveMonIndex = otherPlayerIndex == 0 ? p0ActiveMonIndex : p1ActiveMonIndex; StatBoostToApply[] memory statBoosts = new StatBoostToApply[](1); statBoosts[0] = StatBoostToApply({ stat: MonStateIndexName.SpecialAttack, diff --git a/src/mons/malalien/ActusReus.sol b/src/mons/malalien/ActusReus.sol index df77b7e4..7866e481 100644 --- a/src/mons/malalien/ActusReus.sol +++ b/src/mons/malalien/ActusReus.sol @@ -43,7 +43,7 @@ contract ActusReus is IAbility, BasicEffect { return 0xC0; } - function onAfterMove(uint256, bytes32 extraData, uint256 targetIndex, uint256) + function onAfterMove(bytes32 battleKey, uint256, bytes32 extraData, uint256 targetIndex, uint256, uint256 p0ActiveMonIndex, uint256 p1ActiveMonIndex) external override view @@ -51,11 +51,10 @@ contract ActusReus is IAbility, BasicEffect { { // Check if opposing mon is KOed uint256 otherPlayerIndex = (targetIndex + 1) % 2; - uint256 otherPlayerActiveMonIndex = - ENGINE.getActiveMonIndexForBattleState(ENGINE.battleKeyForWrite())[otherPlayerIndex]; + uint256 otherPlayerActiveMonIndex = otherPlayerIndex == 0 ? p0ActiveMonIndex : p1ActiveMonIndex; bool isOtherMonKOed = ENGINE.getMonStateForBattle( - ENGINE.battleKeyForWrite(), otherPlayerIndex, otherPlayerActiveMonIndex, MonStateIndexName.IsKnockedOut + battleKey, otherPlayerIndex, otherPlayerActiveMonIndex, MonStateIndexName.IsKnockedOut ) == 1; if (isOtherMonKOed) { return (bytes32(uint256(1)), false); @@ -63,7 +62,7 @@ contract ActusReus is IAbility, BasicEffect { return (extraData, false); } - function onAfterDamage(uint256, bytes32 extraData, uint256 targetIndex, uint256 monIndex, int32) + function onAfterDamage(bytes32 battleKey, uint256, bytes32 extraData, uint256 targetIndex, uint256 monIndex, uint256 p0ActiveMonIndex, uint256 p1ActiveMonIndex, int32) external override returns (bytes32, bool) @@ -73,12 +72,11 @@ contract ActusReus is IAbility, BasicEffect { // If we are KO'ed, set a speed delta of half of the opposing mon's base speed bool isKOed = ENGINE.getMonStateForBattle( - ENGINE.battleKeyForWrite(), targetIndex, monIndex, MonStateIndexName.IsKnockedOut + battleKey, targetIndex, monIndex, MonStateIndexName.IsKnockedOut ) == 1; if (isKOed) { uint256 otherPlayerIndex = (targetIndex + 1) % 2; - uint256 otherPlayerActiveMonIndex = - ENGINE.getActiveMonIndexForBattleState(ENGINE.battleKeyForWrite())[otherPlayerIndex]; + uint256 otherPlayerActiveMonIndex = otherPlayerIndex == 0 ? p0ActiveMonIndex : p1ActiveMonIndex; StatBoostToApply[] memory statBoosts = new StatBoostToApply[](1); statBoosts[0] = StatBoostToApply({ stat: MonStateIndexName.Speed, diff --git a/src/mons/malalien/TripleThink.sol b/src/mons/malalien/TripleThink.sol index cae412dd..76022a54 100644 --- a/src/mons/malalien/TripleThink.sol +++ b/src/mons/malalien/TripleThink.sol @@ -25,16 +25,22 @@ contract TripleThink is IMoveSet { return "Triple Think"; } - function move(bytes32 battleKey, uint256 attackerPlayerIndex, uint240, uint256) external { + function move( + bytes32, + uint256 attackerPlayerIndex, + uint256 attackerMonIndex, + uint256, + uint240, + uint256 + ) external { // Apply the buff - uint256 activeMonIndex = ENGINE.getActiveMonIndexForBattleState(battleKey)[attackerPlayerIndex]; StatBoostToApply[] memory statBoosts = new StatBoostToApply[](1); statBoosts[0] = StatBoostToApply({ stat: MonStateIndexName.SpecialAttack, boostPercent: SP_ATTACK_BUFF_PERCENT, boostType: StatBoostType.Multiply }); - STAT_BOOSTS.addStatBoosts(attackerPlayerIndex, activeMonIndex, statBoosts, StatBoostFlag.Temp); + STAT_BOOSTS.addStatBoosts(attackerPlayerIndex, attackerMonIndex, statBoosts, StatBoostFlag.Temp); } function stamina(bytes32, uint256, uint256) external pure returns (uint32) { diff --git a/src/mons/pengym/Deadlift.sol b/src/mons/pengym/Deadlift.sol index 112bec46..147f8ad0 100644 --- a/src/mons/pengym/Deadlift.sol +++ b/src/mons/pengym/Deadlift.sol @@ -26,9 +26,15 @@ contract Deadlift is IMoveSet { return "Deadlift"; } - function move(bytes32 battleKey, uint256 attackerPlayerIndex, uint240, uint256) external { + function move( + bytes32, + uint256 attackerPlayerIndex, + uint256 attackerMonIndex, + uint256, + uint240, + uint256 + ) external { // Apply the buffs - uint256 activeMonIndex = ENGINE.getActiveMonIndexForBattleState(battleKey)[attackerPlayerIndex]; StatBoostToApply[] memory statBoosts = new StatBoostToApply[](2); statBoosts[0] = StatBoostToApply({ stat: MonStateIndexName.Attack, @@ -40,7 +46,7 @@ contract Deadlift is IMoveSet { boostPercent: DEF_BUFF_PERCENT, boostType: StatBoostType.Multiply }); - STAT_BOOSTS.addStatBoosts(attackerPlayerIndex, activeMonIndex, statBoosts, StatBoostFlag.Temp); + STAT_BOOSTS.addStatBoosts(attackerPlayerIndex, attackerMonIndex, statBoosts, StatBoostFlag.Temp); } function stamina(bytes32, uint256, uint256) external pure returns (uint32) { diff --git a/src/mons/pengym/DeepFreeze.sol b/src/mons/pengym/DeepFreeze.sol index 67b05f70..919acec6 100644 --- a/src/mons/pengym/DeepFreeze.sol +++ b/src/mons/pengym/DeepFreeze.sol @@ -43,15 +43,20 @@ contract DeepFreeze is IMoveSet { return -1; } - function move(bytes32 battleKey, uint256 attackerPlayerIndex, uint240, uint256 rng) external { + function move( + bytes32 battleKey, + uint256 attackerPlayerIndex, + uint256, + uint256 defenderMonIndex, + uint240, + uint256 rng + ) external { uint256 otherPlayerIndex = (attackerPlayerIndex + 1) % 2; - uint256 otherPlayerActiveMonIndex = - ENGINE.getActiveMonIndexForBattleState(ENGINE.battleKeyForWrite())[otherPlayerIndex]; uint32 damageToDeal = BASE_POWER; - int32 frostbiteIndex = _frostbiteExists(battleKey, otherPlayerIndex, otherPlayerActiveMonIndex); + int32 frostbiteIndex = _frostbiteExists(battleKey, otherPlayerIndex, defenderMonIndex); // Remove frostbite if it exists, and double the damage dealt if (frostbiteIndex != -1) { - ENGINE.removeEffect(otherPlayerIndex, otherPlayerActiveMonIndex, uint256(uint32(frostbiteIndex))); + ENGINE.removeEffect(otherPlayerIndex, defenderMonIndex, uint256(uint32(frostbiteIndex))); damageToDeal = damageToDeal * 2; } // Deal damage diff --git a/src/mons/pengym/PistolSquat.sol b/src/mons/pengym/PistolSquat.sol index 1b33c453..fe0cfb75 100644 --- a/src/mons/pengym/PistolSquat.sol +++ b/src/mons/pengym/PistolSquat.sol @@ -55,23 +55,25 @@ contract PistolSquat is StandardAttack { return -1; } - function move(bytes32 battleKey, uint256 attackerPlayerIndex, uint240 extraData, uint256 rng) - public - override - { + function move( + bytes32 battleKey, + uint256 attackerPlayerIndex, + uint256 attackerMonIndex, + uint256 defenderMonIndex, + uint240 extraData, + uint256 rng + ) public override { // Deal the damage - super.move(battleKey, attackerPlayerIndex, extraData, rng); + super.move(battleKey, attackerPlayerIndex, attackerMonIndex, defenderMonIndex, extraData, rng); // Deal damage and then force a switch if the opposing mon is not KO'ed uint256 otherPlayerIndex = (attackerPlayerIndex + 1) % 2; - uint256 otherPlayerActiveMonIndex = - ENGINE.getActiveMonIndexForBattleState(ENGINE.battleKeyForWrite())[otherPlayerIndex]; bool isKOed = ENGINE.getMonStateForBattle( - battleKey, otherPlayerIndex, otherPlayerActiveMonIndex, MonStateIndexName.IsKnockedOut + battleKey, otherPlayerIndex, defenderMonIndex, MonStateIndexName.IsKnockedOut ) == 1; if (!isKOed) { - int32 possibleSwitchTarget = _findRandomNonKOedMon(otherPlayerIndex, otherPlayerActiveMonIndex, rng); + int32 possibleSwitchTarget = _findRandomNonKOedMon(otherPlayerIndex, defenderMonIndex, rng); if (possibleSwitchTarget != -1) { ENGINE.switchActiveMon(otherPlayerIndex, uint256(uint32(possibleSwitchTarget))); } diff --git a/src/mons/pengym/PostWorkout.sol b/src/mons/pengym/PostWorkout.sol index 30de00d8..6978188b 100644 --- a/src/mons/pengym/PostWorkout.sol +++ b/src/mons/pengym/PostWorkout.sol @@ -38,12 +38,11 @@ contract PostWorkout is IAbility, BasicEffect { return 0x20; } - function onMonSwitchOut(uint256, bytes32, uint256 targetIndex, uint256 monIndex) + function onMonSwitchOut(bytes32 battleKey, uint256, bytes32, uint256 targetIndex, uint256 monIndex, uint256, uint256) external override returns (bytes32 updatedExtraData, bool removeAfterRun) { - bytes32 battleKey = ENGINE.battleKeyForWrite(); bytes32 keyForMon = StatusEffectLib.getKeyForMonIndex(targetIndex, monIndex); uint192 statusAddress = ENGINE.getGlobalKV(battleKey, keyForMon); diff --git a/src/mons/sofabbi/CarrotHarvest.sol b/src/mons/sofabbi/CarrotHarvest.sol index 895c35f9..2c66931b 100644 --- a/src/mons/sofabbi/CarrotHarvest.sol +++ b/src/mons/sofabbi/CarrotHarvest.sol @@ -43,7 +43,7 @@ contract CarrotHarvest is IAbility, BasicEffect { } // Regain stamina on round end, this can overheal stamina - function onRoundEnd(uint256 rng, bytes32 extraData, uint256 targetIndex, uint256 monIndex) + function onRoundEnd(bytes32, uint256 rng, bytes32 extraData, uint256 targetIndex, uint256 monIndex, uint256, uint256) external override returns (bytes32 updatedExtraData, bool removeAfterRun) diff --git a/src/mons/sofabbi/Gachachacha.sol b/src/mons/sofabbi/Gachachacha.sol index f9aa250d..105ea57a 100644 --- a/src/mons/sofabbi/Gachachacha.sol +++ b/src/mons/sofabbi/Gachachacha.sol @@ -36,22 +36,28 @@ contract Gachachacha is IMoveSet { return "Gachachacha"; } - function move(bytes32 battleKey, uint256 attackerPlayerIndex, uint240, uint256 rng) external { + function move( + bytes32 battleKey, + uint256 attackerPlayerIndex, + uint256 attackerMonIndex, + uint256 defenderMonIndex, + uint240, + uint256 rng + ) external { uint256 chance = rng % OPP_KO_THRESHOLD_R; uint32 basePower; uint256 defenderPlayerIndex = (attackerPlayerIndex + 1) % 2; uint256 playerForCalculator = attackerPlayerIndex; - uint256[] memory activeMon = ENGINE.getActiveMonIndexForBattleState(battleKey); if (chance <= SELF_KO_THRESHOLD_L) { basePower = uint32(chance); } else if (chance > SELF_KO_THRESHOLD_L && chance <= SELF_KO_THRESHOLD_R) { basePower = ENGINE.getMonValueForBattle( - battleKey, attackerPlayerIndex, activeMon[attackerPlayerIndex], MonStateIndexName.Hp + battleKey, attackerPlayerIndex, attackerMonIndex, MonStateIndexName.Hp ); playerForCalculator = defenderPlayerIndex; } else { basePower = ENGINE.getMonValueForBattle( - battleKey, defenderPlayerIndex, activeMon[defenderPlayerIndex], MonStateIndexName.Hp + battleKey, defenderPlayerIndex, defenderMonIndex, MonStateIndexName.Hp ); } AttackCalculator._calculateDamage( diff --git a/src/mons/sofabbi/GuestFeature.sol b/src/mons/sofabbi/GuestFeature.sol index 470064b2..b580cd60 100644 --- a/src/mons/sofabbi/GuestFeature.sol +++ b/src/mons/sofabbi/GuestFeature.sol @@ -25,7 +25,14 @@ contract GuestFeature is IMoveSet { return "Guest Feature"; } - function move(bytes32 battleKey, uint256 attackerPlayerIndex, uint240 extraData, uint256 rng) external { + function move( + bytes32 battleKey, + uint256 attackerPlayerIndex, + uint256, + uint256, + uint240 extraData, + uint256 rng + ) external { uint256 monIndex = uint256(extraData); Type guestType = Type(ENGINE.getMonValueForBattle(battleKey, attackerPlayerIndex, monIndex, MonStateIndexName.Type1)); diff --git a/src/mons/sofabbi/SnackBreak.sol b/src/mons/sofabbi/SnackBreak.sol index 4da12c1f..f766da87 100644 --- a/src/mons/sofabbi/SnackBreak.sol +++ b/src/mons/sofabbi/SnackBreak.sol @@ -33,22 +33,28 @@ contract SnackBreak is IMoveSet { } } - function move(bytes32 battleKey, uint256 attackerPlayerIndex, uint240, uint256) external { - uint256 activeMonIndex = ENGINE.getActiveMonIndexForBattleState(battleKey)[attackerPlayerIndex]; - uint256 snackLevel = _getSnackLevel(battleKey, attackerPlayerIndex, activeMonIndex); - uint32 maxHp = ENGINE.getMonValueForBattle(battleKey, attackerPlayerIndex, activeMonIndex, MonStateIndexName.Hp); + function move( + bytes32 battleKey, + uint256 attackerPlayerIndex, + uint256 attackerMonIndex, + uint256, + uint240, + uint256 + ) external { + uint256 snackLevel = _getSnackLevel(battleKey, attackerPlayerIndex, attackerMonIndex); + uint32 maxHp = ENGINE.getMonValueForBattle(battleKey, attackerPlayerIndex, attackerMonIndex, MonStateIndexName.Hp); // Heal active mon by max HP / 2**snackLevel int32 healAmount = int32(uint32(maxHp / (DEFAULT_HEAL_DENOM * (2 ** snackLevel)))); int32 currentDamage = - ENGINE.getMonStateForBattle(battleKey, attackerPlayerIndex, activeMonIndex, MonStateIndexName.Hp); + ENGINE.getMonStateForBattle(battleKey, attackerPlayerIndex, attackerMonIndex, MonStateIndexName.Hp); if (currentDamage + healAmount > 0) { healAmount = -1 * currentDamage; } - ENGINE.updateMonState(attackerPlayerIndex, activeMonIndex, MonStateIndexName.Hp, healAmount); + ENGINE.updateMonState(attackerPlayerIndex, attackerMonIndex, MonStateIndexName.Hp, healAmount); // Update the snack level - _increaseSnackLevel(battleKey, attackerPlayerIndex, activeMonIndex); + _increaseSnackLevel(battleKey, attackerPlayerIndex, attackerMonIndex); } function stamina(bytes32, uint256, uint256) external pure returns (uint32) { diff --git a/src/mons/volthare/DualShock.sol b/src/mons/volthare/DualShock.sol index 44d634c0..58c899b7 100644 --- a/src/mons/volthare/DualShock.sol +++ b/src/mons/volthare/DualShock.sol @@ -41,18 +41,21 @@ contract DualShock is StandardAttack { OVERCLOCK = _OVERCLOCK; } - function move(bytes32 battleKey, uint256 attackerPlayerIndex, uint240 extraData, uint256 rng) - public - override - { + function move( + bytes32 battleKey, + uint256 attackerPlayerIndex, + uint256 attackerMonIndex, + uint256 defenderMonIndex, + uint240 extraData, + uint256 rng + ) public override { // Deal the damage - super.move(battleKey, attackerPlayerIndex, extraData, rng); + super.move(battleKey, attackerPlayerIndex, attackerMonIndex, defenderMonIndex, extraData, rng); // Apply Zap to self - uint256 activeMonIndex = ENGINE.getActiveMonIndexForBattleState(battleKey)[attackerPlayerIndex]; - ENGINE.addEffect(attackerPlayerIndex, activeMonIndex, ZAP_STATUS, ""); + ENGINE.addEffect(attackerPlayerIndex, attackerMonIndex, ZAP_STATUS, ""); // Apply Overclock to team - OVERCLOCK.applyOverclock(attackerPlayerIndex); + OVERCLOCK.applyOverclock(battleKey, attackerPlayerIndex); } } diff --git a/src/mons/volthare/MegaStarBlast.sol b/src/mons/volthare/MegaStarBlast.sol index 8d4c9578..0f06cd64 100644 --- a/src/mons/volthare/MegaStarBlast.sol +++ b/src/mons/volthare/MegaStarBlast.sol @@ -45,7 +45,14 @@ contract MegaStarBlast is IMoveSet { return -1; } - function move(bytes32 battleKey, uint256 attackerPlayerIndex, uint240, uint256 rng) external { + function move( + bytes32 battleKey, + uint256 attackerPlayerIndex, + uint256, + uint256 defenderMonIndex, + uint240, + uint256 rng + ) external { // Check if Overclock is active uint32 acc = BASE_ACCURACY; int32 overclockIndex = _checkForOverclock(battleKey); @@ -74,8 +81,6 @@ contract MegaStarBlast is IMoveSet { uint256 rng2 = uint256(keccak256(abi.encode(rng))); if (rng2 % 100 < ZAP_ACCURACY) { uint256 defenderPlayerIndex = (attackerPlayerIndex + 1) % 2; - uint256 defenderMonIndex = - ENGINE.getActiveMonIndexForBattleState(ENGINE.battleKeyForWrite())[defenderPlayerIndex]; ENGINE.addEffect(defenderPlayerIndex, defenderMonIndex, ZAP_STATUS, ""); } } diff --git a/src/mons/volthare/RoundTrip.sol b/src/mons/volthare/RoundTrip.sol index 6bf6c708..567db411 100644 --- a/src/mons/volthare/RoundTrip.sol +++ b/src/mons/volthare/RoundTrip.sol @@ -34,12 +34,16 @@ contract RoundTrip is StandardAttack { ) {} - function move(bytes32 battleKey, uint256 attackerPlayerIndex, uint240 extraData, uint256 rng) - public - override - { + function move( + bytes32 battleKey, + uint256 attackerPlayerIndex, + uint256, + uint256 defenderMonIndex, + uint240 extraData, + uint256 rng + ) public override { // Deal the damage - (int32 damage,) = _move(battleKey, attackerPlayerIndex, rng); + (int32 damage,) = _move(battleKey, attackerPlayerIndex, defenderMonIndex, rng); if (damage > 0) { // extraData contains the swap index as raw uint240 diff --git a/src/mons/xmon/ContagiousSlumber.sol b/src/mons/xmon/ContagiousSlumber.sol index 2408f5ce..ecbdc3ed 100644 --- a/src/mons/xmon/ContagiousSlumber.sol +++ b/src/mons/xmon/ContagiousSlumber.sol @@ -22,14 +22,19 @@ contract ContagiousSlumber is IMoveSet { return "Contagious Slumber"; } - function move(bytes32 battleKey, uint256 attackerPlayerIndex, uint240, uint256) external { + function move( + bytes32, + uint256 attackerPlayerIndex, + uint256 attackerMonIndex, + uint256 defenderMonIndex, + uint240, + uint256 + ) external { // Apply sleep to self - uint256 activeMonIndex = ENGINE.getActiveMonIndexForBattleState(battleKey)[attackerPlayerIndex]; - ENGINE.addEffect(attackerPlayerIndex, activeMonIndex, SLEEP_STATUS, ""); + ENGINE.addEffect(attackerPlayerIndex, attackerMonIndex, SLEEP_STATUS, ""); // Apply sleep to opponent uint256 defenderPlayerIndex = (attackerPlayerIndex + 1) % 2; - uint256 defenderMonIndex = ENGINE.getActiveMonIndexForBattleState(battleKey)[defenderPlayerIndex]; ENGINE.addEffect(defenderPlayerIndex, defenderMonIndex, SLEEP_STATUS, ""); } diff --git a/src/mons/xmon/Dreamcatcher.sol b/src/mons/xmon/Dreamcatcher.sol index 4dc9e203..fe3bde9c 100644 --- a/src/mons/xmon/Dreamcatcher.sol +++ b/src/mons/xmon/Dreamcatcher.sol @@ -40,16 +40,18 @@ contract Dreamcatcher is IAbility, BasicEffect { } function onUpdateMonState( + bytes32 battleKey, uint256, bytes32 extraData, uint256 playerIndex, uint256 monIndex, + uint256, + uint256, MonStateIndexName stateVarIndex, int32 valueToAdd ) external override returns (bytes32, bool) { // Only trigger if Stamina is being increased (positive valueToAdd) if (stateVarIndex == MonStateIndexName.Stamina && valueToAdd > 0) { - bytes32 battleKey = ENGINE.battleKeyForWrite(); uint32 maxHp = ENGINE.getMonValueForBattle(battleKey, playerIndex, monIndex, MonStateIndexName.Hp); int32 healAmount = int32(uint32(maxHp)) / HEAL_DENOM; @@ -66,4 +68,3 @@ contract Dreamcatcher is IAbility, BasicEffect { return (extraData, false); } } - diff --git a/src/mons/xmon/NightTerrors.sol b/src/mons/xmon/NightTerrors.sol index 72acc200..d853e62c 100644 --- a/src/mons/xmon/NightTerrors.sol +++ b/src/mons/xmon/NightTerrors.sol @@ -32,8 +32,14 @@ contract NightTerrors is IMoveSet, BasicEffect { return "Night Terrors"; } - function move(bytes32 battleKey, uint256 attackerPlayerIndex, uint240, uint256) external { - uint256 attackerMonIndex = ENGINE.getActiveMonIndexForBattleState(battleKey)[attackerPlayerIndex]; + function move( + bytes32 battleKey, + uint256 attackerPlayerIndex, + uint256 attackerMonIndex, + uint256, + uint240, + uint256 + ) external { uint256 defenderPlayerIndex = (attackerPlayerIndex + 1) % 2; // Check if the effect is already applied to the attacker @@ -104,7 +110,7 @@ contract NightTerrors is IMoveSet, BasicEffect { return 0x24; } - function onRoundEnd(uint256, bytes32 extraData, uint256 targetIndex, uint256 monIndex) + function onRoundEnd(bytes32 battleKey, uint256, bytes32 extraData, uint256 targetIndex, uint256 monIndex, uint256 p0ActiveMonIndex, uint256 p1ActiveMonIndex) external override returns (bytes32, bool) @@ -113,8 +119,6 @@ contract NightTerrors is IMoveSet, BasicEffect { // defenderPlayerIndex is stored in extraData (who should take damage) (uint64 defenderPlayerIndex, uint64 terrorCount) = _unpackExtraData(extraData); - bytes32 battleKey = ENGINE.battleKeyForWrite(); - // Check current stamina of the attacker (who has the effect) int32 staminaDelta = ENGINE.getMonStateForBattle(battleKey, targetIndex, monIndex, MonStateIndexName.Stamina); int32 staminaLeft = int32(ENGINE.getMonStatsForBattle(battleKey, targetIndex, monIndex).stamina) + staminaDelta; @@ -128,7 +132,7 @@ contract NightTerrors is IMoveSet, BasicEffect { ENGINE.updateMonState(targetIndex, monIndex, MonStateIndexName.Stamina, -int32(uint32(terrorCount))); // Get the defender's active mon index - uint256 defenderMonIndex = ENGINE.getActiveMonIndexForBattleState(battleKey)[defenderPlayerIndex]; + uint256 defenderMonIndex = defenderPlayerIndex == 0 ? p0ActiveMonIndex : p1ActiveMonIndex; // Check if opponent (defender) is asleep by iterating through their effects (EffectInstance[] memory defenderEffects, ) = ENGINE.getEffects(battleKey, defenderPlayerIndex, defenderMonIndex); @@ -164,7 +168,7 @@ contract NightTerrors is IMoveSet, BasicEffect { return (extraData, false); } - function onMonSwitchOut(uint256, bytes32 extraData, uint256, uint256) + function onMonSwitchOut(bytes32, uint256, bytes32 extraData, uint256, uint256, uint256, uint256) external pure override diff --git a/src/mons/xmon/Somniphobia.sol b/src/mons/xmon/Somniphobia.sol index 69dc599e..698d890c 100644 --- a/src/mons/xmon/Somniphobia.sol +++ b/src/mons/xmon/Somniphobia.sol @@ -24,7 +24,7 @@ contract Somniphobia is IMoveSet, BasicEffect { return "Somniphobia"; } - function move(bytes32 battleKey, uint256 attackerPlayerIndex, uint240, uint256) external { + function move(bytes32 battleKey, uint256 attackerPlayerIndex, uint256, uint256, uint240, uint256) external { // Add effect globally for 6 turns (only if it's not already in global effects) (EffectInstance[] memory effects, ) = ENGINE.getEffects(battleKey, 2, 2); for (uint256 i = 0; i < effects.length; i++) { @@ -64,12 +64,11 @@ contract Somniphobia is IMoveSet, BasicEffect { return 0x84; } - function onAfterMove(uint256, bytes32 extraData, uint256 targetIndex, uint256 monIndex) + function onAfterMove(bytes32 battleKey, uint256, bytes32 extraData, uint256 targetIndex, uint256 monIndex, uint256, uint256) external override returns (bytes32, bool) { - bytes32 battleKey = ENGINE.battleKeyForWrite(); MoveDecision memory moveDecision = ENGINE.getMoveDecisionForBattleState(battleKey, targetIndex); // Unpack the move index from packedMoveIndex @@ -88,7 +87,7 @@ contract Somniphobia is IMoveSet, BasicEffect { return (extraData, false); } - function onRoundEnd(uint256, bytes32 extraData, uint256, uint256) + function onRoundEnd(bytes32, uint256, bytes32 extraData, uint256, uint256, uint256, uint256) external pure override @@ -102,4 +101,3 @@ contract Somniphobia is IMoveSet, BasicEffect { } } } - diff --git a/src/mons/xmon/VitalSiphon.sol b/src/mons/xmon/VitalSiphon.sol index fc3eb6ff..9b31dee3 100644 --- a/src/mons/xmon/VitalSiphon.sol +++ b/src/mons/xmon/VitalSiphon.sol @@ -36,30 +36,32 @@ contract VitalSiphon is StandardAttack { ) {} - function move(bytes32 battleKey, uint256 attackerPlayerIndex, uint240 extraData, uint256 rng) - public - override - { + function move( + bytes32 battleKey, + uint256 attackerPlayerIndex, + uint256 attackerMonIndex, + uint256 defenderMonIndex, + uint240 extraData, + uint256 rng + ) public override { // Deal the damage - super.move(battleKey, attackerPlayerIndex, extraData, rng); + super.move(battleKey, attackerPlayerIndex, attackerMonIndex, defenderMonIndex, extraData, rng); // 50% chance to steal stamina if (rng % 100 >= STAMINA_STEAL_PERCENT) { uint256 defenderPlayerIndex = (attackerPlayerIndex + 1) % 2; - uint256 defenderMonIndex = ENGINE.getActiveMonIndexForBattleState(battleKey)[defenderPlayerIndex]; - + // Check if opponent has at least 1 stamina int32 defenderStamina = ENGINE.getMonStateForBattle(battleKey, defenderPlayerIndex, defenderMonIndex, MonStateIndexName.Stamina); uint32 defenderBaseStamina = ENGINE.getMonValueForBattle(battleKey, defenderPlayerIndex, defenderMonIndex, MonStateIndexName.Stamina); int32 totalDefenderStamina = int32(defenderBaseStamina) + defenderStamina; - + if (totalDefenderStamina >= 1) { // Steal 1 stamina from opponent ENGINE.updateMonState(defenderPlayerIndex, defenderMonIndex, MonStateIndexName.Stamina, -1); - + // Give 1 stamina to self - uint256 activeMonIndex = ENGINE.getActiveMonIndexForBattleState(battleKey)[attackerPlayerIndex]; - ENGINE.updateMonState(attackerPlayerIndex, activeMonIndex, MonStateIndexName.Stamina, 1); + ENGINE.updateMonState(attackerPlayerIndex, attackerMonIndex, MonStateIndexName.Stamina, 1); } } } diff --git a/src/moves/IMoveSet.sol b/src/moves/IMoveSet.sol index 55d15719..4144faa0 100644 --- a/src/moves/IMoveSet.sol +++ b/src/moves/IMoveSet.sol @@ -6,7 +6,14 @@ import "../Structs.sol"; interface IMoveSet { function name() external view returns (string memory); - function move(bytes32 battleKey, uint256 attackerPlayerIndex, uint240 extraData, uint256 rng) external; + function move( + bytes32 battleKey, + uint256 attackerPlayerIndex, + uint256 attackerMonIndex, + uint256 defenderMonIndex, + uint240 extraData, + uint256 rng + ) external; function priority(bytes32 battleKey, uint256 attackerPlayerIndex) external view returns (uint32); function stamina(bytes32 battleKey, uint256 attackerPlayerIndex, uint256 monIndex) external view returns (uint32); function moveType(bytes32 battleKey) external view returns (Type); diff --git a/src/moves/StandardAttack.sol b/src/moves/StandardAttack.sol index 1e57eeed..c536eba3 100644 --- a/src/moves/StandardAttack.sol +++ b/src/moves/StandardAttack.sol @@ -47,11 +47,18 @@ contract StandardAttack is IMoveSet, Ownable { _initializeOwner(owner); } - function move(bytes32 battleKey, uint256 attackerPlayerIndex, uint240, uint256 rng) public virtual { - _move(battleKey, attackerPlayerIndex, rng); - } - - function _move(bytes32 battleKey, uint256 attackerPlayerIndex, uint256 rng) + function move( + bytes32 battleKey, + uint256 attackerPlayerIndex, + uint256, + uint256 defenderMonIndex, + uint240, + uint256 rng + ) public virtual { + _move(battleKey, attackerPlayerIndex, defenderMonIndex, rng); + } + + function _move(bytes32 battleKey, uint256 attackerPlayerIndex, uint256 defenderMonIndex, uint256 rng) internal virtual returns (int32, bytes32) @@ -78,8 +85,6 @@ contract StandardAttack is IMoveSet, Ownable { // NOTE: technically we should reroll the rng value here instead of using it raw, but the current way that the AttackCalculator works means that if a move misses (e.g. its accuracy is above the threshold), then the effect will not be applied, because it will also fail this check. if (rng % 100 < _effectAccuracy) { uint256 defenderPlayerIndex = (attackerPlayerIndex + 1) % 2; - uint256 defenderMonIndex = - ENGINE.getActiveMonIndexForBattleState(ENGINE.battleKeyForWrite())[defenderPlayerIndex]; if (address(_effect) != address(0)) { ENGINE.addEffect(defenderPlayerIndex, defenderMonIndex, _effect, ""); } diff --git a/test/BattleHistoryTest.sol b/test/BattleHistoryTest.sol index d0ac8e85..8b1c18eb 100644 --- a/test/BattleHistoryTest.sol +++ b/test/BattleHistoryTest.sol @@ -40,7 +40,7 @@ contract BattleHistoryTest is Test, BattleHelper { function setUp() public { defaultOracle = new DefaultRandomnessOracle(); - engine = new Engine(); + engine = new Engine(0, 0, 0); commitManager = new DefaultCommitManager(engine); validator = new DefaultValidator( engine, DefaultValidator.Args({MONS_PER_TEAM: 1, MOVES_PER_MON: 1, TIMEOUT_DURATION: TIMEOUT}) diff --git a/test/CPUTest.sol b/test/CPUTest.sol index 0dcd86bf..a73f4f8e 100644 --- a/test/CPUTest.sol +++ b/test/CPUTest.sol @@ -50,7 +50,7 @@ contract CPUTest is Test { function setUp() public { defaultOracle = new DefaultRandomnessOracle(); - engine = new Engine(); + engine = new Engine(0, 0, 0); commitManager = new DefaultCommitManager(engine); mockCPURNG = new MockCPURNG(); cpu = new RandomCPU(2, engine, mockCPURNG); diff --git a/test/DefaultCommitManagerTest.sol b/test/DefaultCommitManagerTest.sol index fb767509..baa39eb5 100644 --- a/test/DefaultCommitManagerTest.sol +++ b/test/DefaultCommitManagerTest.sol @@ -32,7 +32,7 @@ contract DefaultCommitManagerTest is Test, BattleHelper { function setUp() public { defaultOracle = new DefaultRandomnessOracle(); - engine = new Engine(); + engine = new Engine(0, 0, 0); commitManager = new DefaultCommitManager(engine); validator = new DefaultValidator( engine, DefaultValidator.Args({MONS_PER_TEAM: 1, MOVES_PER_MON: 0, TIMEOUT_DURATION: TIMEOUT}) diff --git a/test/EngineGasTest.sol b/test/EngineGasTest.sol index 504521be..f420ebb6 100644 --- a/test/EngineGasTest.sol +++ b/test/EngineGasTest.sol @@ -20,6 +20,8 @@ import {StaminaRegen} from "../src/effects/StaminaRegen.sol"; import {IMoveSet} from "../src/moves/IMoveSet.sol"; import {DefaultRandomnessOracle} from "../src/rng/DefaultRandomnessOracle.sol"; +import {IRandomnessOracle} from "../src/rng/IRandomnessOracle.sol"; +import {ITeamRegistry} from "../src/teams/ITeamRegistry.sol"; import {ITypeCalculator} from "../src/types/ITypeCalculator.sol"; import {CustomAttack} from "./mocks/CustomAttack.sol"; @@ -55,7 +57,7 @@ contract EngineGasTest is Test, BattleHelper { function setUp() public { defaultOracle = new DefaultRandomnessOracle(); - engine = new Engine(); + engine = new Engine(0, 0, 0); commitManager = new DefaultCommitManager(engine); typeCalc = new TestTypeCalculator(); defaultRegistry = new TestTeamRegistry(); @@ -519,4 +521,187 @@ contract EngineGasTest is Test, BattleHelper { console.log("Execute OVERHEAD:", execute2 - execute1); } } + + /// @notice Compare gas usage between inline validation (address(0) validator) vs external validator + function test_inlineVsExternalValidationGas() public { + // Create engine with proper inline validation defaults + Engine inlineEngine = new Engine(1, 4, 1); + DefaultCommitManager inlineCommitManager = new DefaultCommitManager(inlineEngine); + DefaultMatchmaker inlineMatchmaker = new DefaultMatchmaker(inlineEngine); + + // Create a simple mon with one high-damage move + Mon memory mon = Mon({ + stats: MonStats({hp: 100, stamina: 10, speed: 10, attack: 100, defense: 10, specialAttack: 10, specialDefense: 10, type1: Type.Fire, type2: Type.None}), + moves: new IMoveSet[](4), + ability: IAbility(address(0)) + }); + IMoveSet damageMove = IMoveSet(address(new CustomAttack(inlineEngine, typeCalc, CustomAttack.Args({TYPE: Type.Fire, BASE_POWER: 200, ACCURACY: 100, STAMINA_COST: 0, PRIORITY: 0})))); + mon.moves[0] = damageMove; + mon.moves[1] = damageMove; + mon.moves[2] = damageMove; + mon.moves[3] = damageMove; + + Mon[] memory team = new Mon[](1); + team[0] = mon; + defaultRegistry.setTeam(ALICE, team); + defaultRegistry.setTeam(BOB, team); + + // Create validator for external validation path + DefaultValidator externalValidator = new DefaultValidator( + IEngine(address(inlineEngine)), DefaultValidator.Args({MONS_PER_TEAM: 1, MOVES_PER_MON: 4, TIMEOUT_DURATION: 10}) + ); + + IEffect[] memory noEffects = new IEffect[](0); + IRuleset simpleRuleset = IRuleset(address(new DefaultRuleset(inlineEngine, noEffects))); + + // === EXTERNAL VALIDATION PATH === + vm.startSnapshotGas("External_Setup"); + bytes32 externalBattleKey = _startBattleForEngine( + externalValidator, + inlineEngine, + defaultOracle, + defaultRegistry, + inlineMatchmaker, + new IEngineHook[](0), + simpleRuleset, + address(inlineCommitManager) + ); + uint256 externalSetup = vm.stopSnapshotGas("External_Setup"); + + vm.warp(vm.getBlockTimestamp() + 1); + + vm.startSnapshotGas("External_Execute"); + _commitRevealExecuteForEngine(inlineEngine, inlineCommitManager, externalBattleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0)); + _commitRevealExecuteForEngine(inlineEngine, inlineCommitManager, externalBattleKey, 0, 0, 0, 0); + uint256 externalExecute = vm.stopSnapshotGas("External_Execute"); + + // === INLINE VALIDATION PATH === + vm.startSnapshotGas("Inline_Setup"); + bytes32 inlineBattleKey = _startBattleForEngine( + IValidator(address(0)), // Inline validation! + inlineEngine, + defaultOracle, + defaultRegistry, + inlineMatchmaker, + new IEngineHook[](0), + simpleRuleset, + address(inlineCommitManager) + ); + uint256 inlineSetup = vm.stopSnapshotGas("Inline_Setup"); + + vm.warp(vm.getBlockTimestamp() + 1); + + vm.startSnapshotGas("Inline_Execute"); + _commitRevealExecuteForEngine(inlineEngine, inlineCommitManager, inlineBattleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0)); + _commitRevealExecuteForEngine(inlineEngine, inlineCommitManager, inlineBattleKey, 0, 0, 0, 0); + uint256 inlineExecute = vm.stopSnapshotGas("Inline_Execute"); + + console.log("========================================"); + console.log("INLINE vs EXTERNAL VALIDATION BENCHMARK"); + console.log("========================================"); + console.log(""); + console.log("--- SETUP (startBattle) ---"); + console.log("External Validator Setup:", externalSetup); + console.log("Inline Validation Setup:", inlineSetup); + if (inlineSetup < externalSetup) { + console.log("Inline SAVES:", externalSetup - inlineSetup); + } else { + console.log("Inline COSTS MORE:", inlineSetup - externalSetup); + } + console.log(""); + console.log("--- EXECUTE (switch + attack) ---"); + console.log("External Validator Execute:", externalExecute); + console.log("Inline Validation Execute:", inlineExecute); + if (inlineExecute < externalExecute) { + console.log("Inline SAVES:", externalExecute - inlineExecute); + console.log("Percentage saved:", (externalExecute - inlineExecute) * 100 / externalExecute, "%"); + } else { + console.log("Inline COSTS MORE:", inlineExecute - externalExecute); + } + console.log("========================================"); + } + + // Helper to start battle with a specific engine + function _startBattleForEngine( + IValidator validator, + Engine eng, + IRandomnessOracle rngOracle, + ITeamRegistry registry, + DefaultMatchmaker maker, + IEngineHook[] memory hooks, + IRuleset ruleset, + address moveManager + ) internal returns (bytes32) { + vm.startPrank(ALICE); + address[] memory makersToAdd = new address[](1); + makersToAdd[0] = address(maker); + address[] memory makersToRemove = new address[](0); + eng.updateMatchmakers(makersToAdd, makersToRemove); + + vm.startPrank(BOB); + eng.updateMatchmakers(makersToAdd, makersToRemove); + + bytes32 salt = ""; + uint96 p0TeamIndex = 0; + uint256[] memory p0TeamIndices = registry.getMonRegistryIndicesForTeam(ALICE, p0TeamIndex); + bytes32 p0TeamHash = keccak256(abi.encodePacked(salt, p0TeamIndex, p0TeamIndices)); + + ProposedBattle memory proposal = ProposedBattle({ + p0: ALICE, + p0TeamIndex: 0, + p0TeamHash: p0TeamHash, + p1: BOB, + p1TeamIndex: 0, + teamRegistry: registry, + validator: validator, + rngOracle: rngOracle, + ruleset: ruleset, + engineHooks: hooks, + moveManager: moveManager, + matchmaker: maker + }); + + vm.startPrank(ALICE); + bytes32 battleKey = maker.proposeBattle(proposal); + + bytes32 battleIntegrityHash = maker.getBattleProposalIntegrityHash(proposal); + vm.startPrank(BOB); + maker.acceptBattle(battleKey, 0, battleIntegrityHash); + + vm.startPrank(ALICE); + maker.confirmBattle(battleKey, salt, p0TeamIndex); + + return battleKey; + } + + // Helper to commit/reveal/execute for a specific engine + function _commitRevealExecuteForEngine( + Engine eng, + DefaultCommitManager cm, + bytes32 battleKey, + uint8 aliceMoveIndex, + uint8 bobMoveIndex, + uint240 aliceExtraData, + uint240 bobExtraData + ) internal { + bytes32 salt = ""; + bytes32 aliceMoveHash = keccak256(abi.encodePacked(aliceMoveIndex, salt, aliceExtraData)); + bytes32 bobMoveHash = keccak256(abi.encodePacked(bobMoveIndex, salt, bobExtraData)); + uint256 turnId = eng.getTurnIdForBattleState(battleKey); + if (turnId % 2 == 0) { + vm.startPrank(ALICE); + cm.commitMove(battleKey, aliceMoveHash); + vm.startPrank(BOB); + cm.revealMove(battleKey, bobMoveIndex, salt, bobExtraData, true); + vm.startPrank(ALICE); + cm.revealMove(battleKey, aliceMoveIndex, salt, aliceExtraData, true); + } else { + vm.startPrank(BOB); + cm.commitMove(battleKey, bobMoveHash); + vm.startPrank(ALICE); + cm.revealMove(battleKey, aliceMoveIndex, salt, aliceExtraData, true); + vm.startPrank(BOB); + cm.revealMove(battleKey, bobMoveIndex, salt, bobExtraData, true); + } + } } \ No newline at end of file diff --git a/test/EngineTest.sol b/test/EngineTest.sol index 3e2cbeac..f2ebc5f0 100644 --- a/test/EngineTest.sol +++ b/test/EngineTest.sol @@ -69,7 +69,7 @@ contract EngineTest is Test, BattleHelper { function setUp() public { defaultOracle = new DefaultRandomnessOracle(); - engine = new Engine(); + engine = new Engine(0, 0, 0); commitManager = new DefaultCommitManager(engine); validator = new DefaultValidator( engine, DefaultValidator.Args({MONS_PER_TEAM: 1, MOVES_PER_MON: 1, TIMEOUT_DURATION: TIMEOUT_DURATION}) diff --git a/test/GachaTeamRegistryTest.sol b/test/GachaTeamRegistryTest.sol index 126a5366..0661b850 100644 --- a/test/GachaTeamRegistryTest.sol +++ b/test/GachaTeamRegistryTest.sol @@ -38,7 +38,7 @@ contract GachaTeamRegistryTest is Test { function setUp() public { monRegistry = new DefaultMonRegistry(); - engine = new Engine(); + engine = new Engine(0, 0, 0); mockRNG = new MockGachaRNG(); gachaRegistry = new GachaRegistry(monRegistry, engine, mockRNG, 1); diff --git a/test/GachaTest.sol b/test/GachaTest.sol index 3bcb0952..51c5f656 100644 --- a/test/GachaTest.sol +++ b/test/GachaTest.sol @@ -30,7 +30,7 @@ contract GachaTest is Test, BattleHelper { function setUp() public { defaultOracle = new DefaultRandomnessOracle(); - engine = new Engine(); + engine = new Engine(0, 0, 0); commitManager = new DefaultCommitManager(engine); defaultRegistry = new TestTeamRegistry(); monRegistry = new DefaultMonRegistry(); diff --git a/test/InlineEngineGasTest.sol b/test/InlineEngineGasTest.sol new file mode 100644 index 00000000..3b7f7c23 --- /dev/null +++ b/test/InlineEngineGasTest.sol @@ -0,0 +1,536 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity ^0.8.0; + +import "../lib/forge-std/src/Test.sol"; + +import "../src/Constants.sol"; +import "../src/Enums.sol"; +import "../src/Structs.sol"; + +import {DefaultRuleset} from "../src/DefaultRuleset.sol"; + +import {DefaultCommitManager} from "../src/commit-manager/DefaultCommitManager.sol"; +import {Engine} from "../src/Engine.sol"; +import {IEngine} from "../src/IEngine.sol"; +import {IValidator} from "../src/IValidator.sol"; +import {IAbility} from "../src/abilities/IAbility.sol"; + +import {IEffect} from "../src/effects/IEffect.sol"; +import {StaminaRegen} from "../src/effects/StaminaRegen.sol"; + +import {IMoveSet} from "../src/moves/IMoveSet.sol"; +import {DefaultRandomnessOracle} from "../src/rng/DefaultRandomnessOracle.sol"; +import {IRandomnessOracle} from "../src/rng/IRandomnessOracle.sol"; +import {ITeamRegistry} from "../src/teams/ITeamRegistry.sol"; +import {ITypeCalculator} from "../src/types/ITypeCalculator.sol"; +import {CustomAttack} from "./mocks/CustomAttack.sol"; + +import {EffectAttack} from "./mocks/EffectAttack.sol"; +import {StatBoostsMove} from "./mocks/StatBoostsMove.sol"; + +import {BurnStatus} from "../src/effects/status/BurnStatus.sol"; +import {FrostbiteStatus} from "../src/effects/status/FrostbiteStatus.sol"; +import {StatBoosts} from "../src/effects/StatBoosts.sol"; + +import {IEngineHook} from "../src/IEngineHook.sol"; + +import {SingleInstanceEffect} from "./mocks/SingleInstanceEffect.sol"; +import {TestTeamRegistry} from "./mocks/TestTeamRegistry.sol"; + +import {DefaultMatchmaker} from "../src/matchmaker/DefaultMatchmaker.sol"; +import {BattleHelper} from "./abstract/BattleHelper.sol"; +import {TestTypeCalculator} from "./mocks/TestTypeCalculator.sol"; + +/// @title Inline Engine Gas Test +/// @notice Same as EngineGasTest but uses inline validation (address(0) validator) for comparison +contract InlineEngineGasTest is Test, BattleHelper { + + DefaultCommitManager commitManager; + Engine engine; + ITypeCalculator typeCalc; + DefaultRandomnessOracle defaultOracle; + TestTeamRegistry defaultRegistry; + DefaultMatchmaker matchmaker; + + // Inline validation constants + uint256 constant MONS_PER_TEAM = 4; + uint256 constant MOVES_PER_MON = 4; + + function _packStatBoost(uint256 playerIndex, uint256 monIndex, uint256 statIndex, int32 boostAmount) internal pure returns (uint240) { + return uint240(playerIndex | (monIndex << 60) | (statIndex << 120) | (uint256(uint32(boostAmount)) << 180)); + } + + function setUp() public { + defaultOracle = new DefaultRandomnessOracle(); + // Create engine with inline validation defaults + engine = new Engine(MONS_PER_TEAM, MOVES_PER_MON, 1); + commitManager = new DefaultCommitManager(engine); + typeCalc = new TestTypeCalculator(); + defaultRegistry = new TestTeamRegistry(); + matchmaker = new DefaultMatchmaker(engine); + } + + /// @notice Helper to start battle with inline validation (address(0) validator) + function _startBattleInline( + Engine eng, + IRandomnessOracle rngOracle, + ITeamRegistry registry, + DefaultMatchmaker maker, + IEngineHook[] memory hooks, + IRuleset ruleset, + address moveManager + ) internal returns (bytes32) { + vm.startPrank(ALICE); + address[] memory makersToAdd = new address[](1); + makersToAdd[0] = address(maker); + address[] memory makersToRemove = new address[](0); + eng.updateMatchmakers(makersToAdd, makersToRemove); + + vm.startPrank(BOB); + eng.updateMatchmakers(makersToAdd, makersToRemove); + + bytes32 salt = ""; + uint96 p0TeamIndex = 0; + uint256[] memory p0TeamIndices = registry.getMonRegistryIndicesForTeam(ALICE, p0TeamIndex); + bytes32 p0TeamHash = keccak256(abi.encodePacked(salt, p0TeamIndex, p0TeamIndices)); + + ProposedBattle memory proposal = ProposedBattle({ + p0: ALICE, + p0TeamIndex: 0, + p0TeamHash: p0TeamHash, + p1: BOB, + p1TeamIndex: 0, + teamRegistry: registry, + validator: IValidator(address(0)), // INLINE VALIDATION + rngOracle: rngOracle, + ruleset: ruleset, + engineHooks: hooks, + moveManager: moveManager, + matchmaker: maker + }); + + vm.startPrank(ALICE); + bytes32 battleKey = maker.proposeBattle(proposal); + + bytes32 battleIntegrityHash = maker.getBattleProposalIntegrityHash(proposal); + vm.startPrank(BOB); + maker.acceptBattle(battleKey, 0, battleIntegrityHash); + + vm.startPrank(ALICE); + maker.confirmBattle(battleKey, salt, p0TeamIndex); + + return battleKey; + } + + function test_consecutiveBattleGas() public { + Mon memory mon = _createMon(); + mon.stats.stamina = 5; + mon.stats.attack = 10; + mon.stats.specialAttack = 10; + + mon.moves = new IMoveSet[](4); + StatBoosts statBoosts = new StatBoosts(engine); + IMoveSet burnMove = new EffectAttack(engine, new BurnStatus(engine, statBoosts), EffectAttack.Args({TYPE: Type.Fire, STAMINA_COST: 1, PRIORITY: 1})); + IMoveSet frostbiteMove = new EffectAttack(engine, new FrostbiteStatus(engine, statBoosts), EffectAttack.Args({TYPE: Type.Fire, STAMINA_COST: 1, PRIORITY: 1})); + IMoveSet statBoostMove = new StatBoostsMove(engine, statBoosts); + IMoveSet damageMove = new CustomAttack(engine, ITypeCalculator(address(typeCalc)), CustomAttack.Args({TYPE: Type.Fire, BASE_POWER: 10, ACCURACY: 100, STAMINA_COST: 1, PRIORITY: 1})); + mon.moves[0] = burnMove; + mon.moves[1] = frostbiteMove; + mon.moves[2] = statBoostMove; + mon.moves[3] = damageMove; + + Mon[] memory team = new Mon[](4); + for (uint256 i = 0; i < team.length; i++) { + team[i] = mon; + } + defaultRegistry.setTeam(ALICE, team); + defaultRegistry.setTeam(BOB, team); + + StaminaRegen staminaRegen = new StaminaRegen(engine); + IEffect[] memory effects = new IEffect[](1); + effects[0] = staminaRegen; + DefaultRuleset ruleset = new DefaultRuleset(IEngine(address(engine)), effects); + + vm.startSnapshotGas("Setup 1"); + bytes32 battleKey = _startBattleInline(engine, defaultOracle, defaultRegistry, matchmaker, new IEngineHook[](0), IRuleset(address(ruleset)), address(commitManager)); + uint256 setup1Gas = vm.stopSnapshotGas("Setup 1"); + + vm.warp(vm.getBlockTimestamp() + 1); + + vm.startSnapshotGas("FirstBattle"); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0)); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 0, 1, 0, 0); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, SWITCH_MOVE_INDEX, 2, uint240(1), _packStatBoost(1, 0, uint256(MonStateIndexName.Attack), int32(90))); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 2, 3, _packStatBoost(0, 1, uint256(MonStateIndexName.Attack), int32(90)), 0); + vm.startPrank(ALICE); + commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(0), true); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 2, NO_OP_MOVE_INDEX, _packStatBoost(0, 0, uint256(MonStateIndexName.Attack), int32(90)), 0); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 3, NO_OP_MOVE_INDEX, 0, 0); + vm.startPrank(BOB); + commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(1), true); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, NO_OP_MOVE_INDEX, 2, 0, _packStatBoost(1, 1, uint256(MonStateIndexName.Attack), int32(90))); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, NO_OP_MOVE_INDEX, 3, 0, 0); + vm.startPrank(ALICE); + commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(2), true); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, NO_OP_MOVE_INDEX, 3, 0, 0); + vm.startPrank(ALICE); + commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(3), true); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, NO_OP_MOVE_INDEX, 3, 0, 0); + uint256 firstBattleGas = vm.stopSnapshotGas("FirstBattle"); + + // Rearrange order of moves for battle 2 + mon.moves[1] = burnMove; + mon.moves[2] = frostbiteMove; + mon.moves[3] = statBoostMove; + mon.moves[0] = damageMove; + for (uint256 i = 0; i < team.length; i++) { + team[i] = mon; + } + defaultRegistry.setTeam(ALICE, team); + defaultRegistry.setTeam(BOB, team); + + vm.startSnapshotGas("Setup 2"); + bytes32 battleKey2 = _startBattleInline(engine, defaultOracle, defaultRegistry, matchmaker, new IEngineHook[](0), IRuleset(address(ruleset)), address(commitManager)); + uint256 setup2Gas = vm.stopSnapshotGas("Setup 2"); + + vm.warp(vm.getBlockTimestamp() + 1); + + (BattleConfigView memory cfgAfterSetup2,) = engine.getBattle(battleKey2); + console.log("After setup 2 - globalEffectsLength:", cfgAfterSetup2.globalEffectsLength); + console.log("After setup 2 - packedP0EffectsCount:", cfgAfterSetup2.packedP0EffectsCount); + console.log("After setup 2 - packedP1EffectsCount:", cfgAfterSetup2.packedP1EffectsCount); + + vm.startSnapshotGas("SecondBattle"); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey2, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0)); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey2, 3, 1, _packStatBoost(0, 0, uint256(MonStateIndexName.Attack), int32(90)), 0); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey2, 0, NO_OP_MOVE_INDEX, 0, 0); + vm.startPrank(BOB); + commitManager.revealMove(battleKey2, SWITCH_MOVE_INDEX, 0, uint240(1), true); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey2, SWITCH_MOVE_INDEX, 2, uint240(1), 0); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey2, 3, NO_OP_MOVE_INDEX, _packStatBoost(0, 1, uint256(MonStateIndexName.Attack), int32(90)), 0); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey2, 0, NO_OP_MOVE_INDEX, 0, 0); + vm.startPrank(BOB); + commitManager.revealMove(battleKey2, SWITCH_MOVE_INDEX, 0, uint240(2), true); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey2, NO_OP_MOVE_INDEX, 3, 0, _packStatBoost(1, 2, uint256(MonStateIndexName.Attack), int32(90))); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey2, NO_OP_MOVE_INDEX, 0, 0, 0); + vm.startPrank(ALICE); + commitManager.revealMove(battleKey2, SWITCH_MOVE_INDEX, 0, uint240(2), true); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey2, 3, 3, _packStatBoost(0, 2, uint256(MonStateIndexName.Attack), int32(90)), _packStatBoost(1, 2, uint256(MonStateIndexName.Attack), int32(90))); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey2, 0, NO_OP_MOVE_INDEX, 0, 0); + vm.startPrank(BOB); + commitManager.revealMove(battleKey2, SWITCH_MOVE_INDEX, 0, uint240(3), true); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey2, 0, NO_OP_MOVE_INDEX, 0, 0); + uint256 secondBattleGas = vm.stopSnapshotGas("SecondBattle"); + + // Battle 3: Repeat exact sequence of Battle 1 + mon.moves[0] = burnMove; + mon.moves[1] = frostbiteMove; + mon.moves[2] = statBoostMove; + mon.moves[3] = damageMove; + for (uint256 i = 0; i < team.length; i++) { + team[i] = mon; + } + defaultRegistry.setTeam(ALICE, team); + defaultRegistry.setTeam(BOB, team); + + vm.startSnapshotGas("Setup 3"); + bytes32 battleKey3 = _startBattleInline(engine, defaultOracle, defaultRegistry, matchmaker, new IEngineHook[](0), IRuleset(address(ruleset)), address(commitManager)); + uint256 setup3Gas = vm.stopSnapshotGas("Setup 3"); + + vm.warp(vm.getBlockTimestamp() + 1); + + vm.startSnapshotGas("ThirdBattle"); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey3, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0)); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey3, 0, 1, 0, 0); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey3, SWITCH_MOVE_INDEX, 2, uint240(1), _packStatBoost(1, 0, uint256(MonStateIndexName.Attack), int32(90))); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey3, 2, 3, _packStatBoost(0, 1, uint256(MonStateIndexName.Attack), int32(90)), 0); + vm.startPrank(ALICE); + commitManager.revealMove(battleKey3, SWITCH_MOVE_INDEX, 0, uint240(0), true); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey3, 2, NO_OP_MOVE_INDEX, _packStatBoost(0, 0, uint256(MonStateIndexName.Attack), int32(90)), 0); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey3, 3, NO_OP_MOVE_INDEX, 0, 0); + vm.startPrank(BOB); + commitManager.revealMove(battleKey3, SWITCH_MOVE_INDEX, 0, uint240(1), true); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey3, NO_OP_MOVE_INDEX, 2, 0, _packStatBoost(1, 1, uint256(MonStateIndexName.Attack), int32(90))); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey3, NO_OP_MOVE_INDEX, 3, 0, 0); + vm.startPrank(ALICE); + commitManager.revealMove(battleKey3, SWITCH_MOVE_INDEX, 0, uint240(2), true); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey3, NO_OP_MOVE_INDEX, 3, 0, 0); + vm.startPrank(ALICE); + commitManager.revealMove(battleKey3, SWITCH_MOVE_INDEX, 0, uint240(3), true); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey3, NO_OP_MOVE_INDEX, 3, 0, 0); + uint256 thirdBattleGas = vm.stopSnapshotGas("ThirdBattle"); + + console.log("=== INLINE VALIDATION Gas Results ==="); + console.log("Setup 1 Gas:", setup1Gas); + console.log("Setup 2 Gas:", setup2Gas); + console.log("Setup 3 Gas:", setup3Gas); + console.log("Battle 1 Gas:", firstBattleGas); + console.log("Battle 2 Gas:", secondBattleGas); + console.log("Battle 3 Gas:", thirdBattleGas); + + assertLt(setup2Gas, setup1Gas, "Setup 2 should be cheaper (storage reuse)"); + assertLt(setup3Gas, setup1Gas, "Setup 3 should be cheaper (storage reuse)"); + + console.log("=== Battle Comparisons ==="); + if (secondBattleGas > firstBattleGas) { + console.log("Battle 2 vs 1: MORE expensive by:", secondBattleGas - firstBattleGas); + } else { + console.log("Battle 2 vs 1: LESS expensive by:", firstBattleGas - secondBattleGas); + } + if (thirdBattleGas > firstBattleGas) { + console.log("Battle 3 vs 1: MORE expensive by:", thirdBattleGas - firstBattleGas); + } else { + console.log("Battle 3 vs 1: LESS expensive by:", firstBattleGas - thirdBattleGas); + } + console.log("Battle 3 savings vs Battle 1:", firstBattleGas > thirdBattleGas ? firstBattleGas - thirdBattleGas : 0); + } + + function test_identicalBattlesGas() public { + // Note: We need to recreate engine with correct team size for inline validation + // Important: Create engine BEFORE moves so moves reference the correct engine + Engine inlineEngine = new Engine(1, 4, 1); + + Mon memory mon = Mon({ + stats: MonStats({hp: 100, stamina: 10, speed: 10, attack: 100, defense: 10, specialAttack: 10, specialDefense: 10, type1: Type.Fire, type2: Type.None}), + moves: new IMoveSet[](4), + ability: IAbility(address(0)) + }); + + // Use inlineEngine for moves so they reference the correct engine + IMoveSet damageMove = IMoveSet(address(new CustomAttack(inlineEngine, typeCalc, CustomAttack.Args({TYPE: Type.Fire, BASE_POWER: 200, ACCURACY: 100, STAMINA_COST: 0, PRIORITY: 0})))); + mon.moves[0] = damageMove; + mon.moves[1] = damageMove; + mon.moves[2] = damageMove; + mon.moves[3] = damageMove; + + Mon[] memory team = new Mon[](1); + team[0] = mon; + + defaultRegistry.setTeam(ALICE, team); + defaultRegistry.setTeam(BOB, team); + DefaultCommitManager inlineCommitManager = new DefaultCommitManager(inlineEngine); + DefaultMatchmaker inlineMatchmaker = new DefaultMatchmaker(inlineEngine); + + IEffect[] memory noEffects = new IEffect[](0); + IRuleset simpleRuleset = IRuleset(address(new DefaultRuleset(inlineEngine, noEffects))); + + // Battle 1: Fresh storage + vm.startSnapshotGas("Battle1_Setup"); + bytes32 battleKey1 = _startBattleInlineCustomEngine(inlineEngine, defaultOracle, defaultRegistry, inlineMatchmaker, new IEngineHook[](0), simpleRuleset, address(inlineCommitManager)); + uint256 setup1 = vm.stopSnapshotGas("Battle1_Setup"); + + vm.warp(vm.getBlockTimestamp() + 1); + + vm.startSnapshotGas("Battle1_Execute"); + _commitRevealExecuteForEngine(inlineEngine, inlineCommitManager, battleKey1, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0)); + _commitRevealExecuteForEngine(inlineEngine, inlineCommitManager, battleKey1, 0, 0, 0, 0); + uint256 execute1 = vm.stopSnapshotGas("Battle1_Execute"); + + // Battle 2: Reusing storage + vm.startSnapshotGas("Battle2_Setup"); + bytes32 battleKey2 = _startBattleInlineCustomEngine(inlineEngine, defaultOracle, defaultRegistry, inlineMatchmaker, new IEngineHook[](0), simpleRuleset, address(inlineCommitManager)); + uint256 setup2 = vm.stopSnapshotGas("Battle2_Setup"); + + vm.warp(vm.getBlockTimestamp() + 1); + + vm.startSnapshotGas("Battle2_Execute"); + _commitRevealExecuteForEngine(inlineEngine, inlineCommitManager, battleKey2, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0)); + _commitRevealExecuteForEngine(inlineEngine, inlineCommitManager, battleKey2, 0, 0, 0, 0); + uint256 execute2 = vm.stopSnapshotGas("Battle2_Execute"); + + console.log("=== INLINE Identical Battles Test ==="); + console.log("Setup 1:", setup1); + console.log("Setup 2:", setup2); + console.log("Execute 1:", execute1); + console.log("Execute 2:", execute2); + + if (setup2 < setup1) { + console.log("Setup savings:", setup1 - setup2); + } + if (execute2 < execute1) { + console.log("Execute savings:", execute1 - execute2); + } else { + console.log("Execute OVERHEAD:", execute2 - execute1); + } + } + + function test_identicalBattlesWithEffectsGas() public { + Mon memory mon = Mon({ + stats: MonStats({hp: 100, stamina: 100, speed: 10, attack: 100, defense: 10, specialAttack: 10, specialDefense: 10, type1: Type.Fire, type2: Type.None}), + moves: new IMoveSet[](4), + ability: IAbility(address(0)) + }); + + // Recreate engine with correct team size + Engine inlineEngine = new Engine(1, 4, 1); + DefaultCommitManager inlineCommitManager = new DefaultCommitManager(inlineEngine); + DefaultMatchmaker inlineMatchmaker = new DefaultMatchmaker(inlineEngine); + + StatBoosts statBoosts = new StatBoosts(inlineEngine); + IMoveSet effectMove = new EffectAttack( + inlineEngine, + new SingleInstanceEffect(inlineEngine), + EffectAttack.Args({TYPE: Type.Fire, STAMINA_COST: 1, PRIORITY: 1}) + ); + IMoveSet damageMove = IMoveSet(address(new CustomAttack(inlineEngine, typeCalc, CustomAttack.Args({TYPE: Type.Fire, BASE_POWER: 200, ACCURACY: 100, STAMINA_COST: 0, PRIORITY: 0})))); + mon.moves[0] = effectMove; + mon.moves[1] = damageMove; + mon.moves[2] = damageMove; + mon.moves[3] = damageMove; + + Mon[] memory team = new Mon[](1); + team[0] = mon; + + defaultRegistry.setTeam(ALICE, team); + defaultRegistry.setTeam(BOB, team); + + StaminaRegen staminaRegen = new StaminaRegen(inlineEngine); + IEffect[] memory effects = new IEffect[](1); + effects[0] = staminaRegen; + IRuleset rulesetWithEffect = IRuleset(address(new DefaultRuleset(inlineEngine, effects))); + + // Battle 1: Fresh storage + vm.startSnapshotGas("B1_Setup"); + bytes32 battleKey1 = _startBattleInlineCustomEngine(inlineEngine, defaultOracle, defaultRegistry, inlineMatchmaker, new IEngineHook[](0), rulesetWithEffect, address(inlineCommitManager)); + uint256 setup1 = vm.stopSnapshotGas("B1_Setup"); + + vm.warp(vm.getBlockTimestamp() + 1); + + vm.startSnapshotGas("B1_Execute"); + _commitRevealExecuteForEngine(inlineEngine, inlineCommitManager, battleKey1, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0)); + + (BattleConfigView memory cfgAfterSwitch,) = inlineEngine.getBattle(battleKey1); + console.log("After B1 switch - globalEffectsLength:", cfgAfterSwitch.globalEffectsLength); + console.log("After B1 switch - packedP0EffectsCount:", cfgAfterSwitch.packedP0EffectsCount); + console.log("After B1 switch - packedP1EffectsCount:", cfgAfterSwitch.packedP1EffectsCount); + + _commitRevealExecuteForEngine(inlineEngine, inlineCommitManager, battleKey1, 0, 0, 0, 0); + + (BattleConfigView memory cfgAfterEffects,) = inlineEngine.getBattle(battleKey1); + console.log("After B1 effects - globalEffectsLength:", cfgAfterEffects.globalEffectsLength); + console.log("After B1 effects - packedP0EffectsCount:", cfgAfterEffects.packedP0EffectsCount); + console.log("After B1 effects - packedP1EffectsCount:", cfgAfterEffects.packedP1EffectsCount); + + _commitRevealExecuteForEngine(inlineEngine, inlineCommitManager, battleKey1, 1, 1, 0, 0); + uint256 execute1 = vm.stopSnapshotGas("B1_Execute"); + + (, BattleData memory data1) = inlineEngine.getBattle(battleKey1); + console.log("Battle 1 winner index:", data1.winnerIndex); + + // Battle 2: Reusing storage + vm.startSnapshotGas("B2_Setup"); + bytes32 battleKey2 = _startBattleInlineCustomEngine(inlineEngine, defaultOracle, defaultRegistry, inlineMatchmaker, new IEngineHook[](0), rulesetWithEffect, address(inlineCommitManager)); + uint256 setup2 = vm.stopSnapshotGas("B2_Setup"); + + vm.warp(vm.getBlockTimestamp() + 1); + + (BattleConfigView memory cfg2,) = inlineEngine.getBattle(battleKey2); + console.log("After B2 setup - globalEffectsLength:", cfg2.globalEffectsLength); + console.log("After B2 setup - packedP0EffectsCount:", cfg2.packedP0EffectsCount); + console.log("After B2 setup - packedP1EffectsCount:", cfg2.packedP1EffectsCount); + + vm.startSnapshotGas("B2_Execute"); + _commitRevealExecuteForEngine(inlineEngine, inlineCommitManager, battleKey2, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0)); + _commitRevealExecuteForEngine(inlineEngine, inlineCommitManager, battleKey2, 0, 0, 0, 0); + _commitRevealExecuteForEngine(inlineEngine, inlineCommitManager, battleKey2, 1, 1, 0, 0); + uint256 execute2 = vm.stopSnapshotGas("B2_Execute"); + + console.log("=== INLINE Battles With Effects ==="); + console.log("Setup 1:", setup1); + console.log("Setup 2:", setup2); + console.log("Execute 1:", execute1); + console.log("Execute 2:", execute2); + + if (setup2 < setup1) { + console.log("Setup savings:", setup1 - setup2); + } + if (execute2 < execute1) { + console.log("Execute savings:", execute1 - execute2); + } else { + console.log("Execute OVERHEAD:", execute2 - execute1); + } + } + + // Helper to start battle with inline validation for a custom engine + function _startBattleInlineCustomEngine( + Engine eng, + IRandomnessOracle rngOracle, + ITeamRegistry registry, + DefaultMatchmaker maker, + IEngineHook[] memory hooks, + IRuleset ruleset, + address moveManager + ) internal returns (bytes32) { + vm.startPrank(ALICE); + address[] memory makersToAdd = new address[](1); + makersToAdd[0] = address(maker); + address[] memory makersToRemove = new address[](0); + eng.updateMatchmakers(makersToAdd, makersToRemove); + + vm.startPrank(BOB); + eng.updateMatchmakers(makersToAdd, makersToRemove); + + bytes32 salt = ""; + uint96 p0TeamIndex = 0; + uint256[] memory p0TeamIndices = registry.getMonRegistryIndicesForTeam(ALICE, p0TeamIndex); + bytes32 p0TeamHash = keccak256(abi.encodePacked(salt, p0TeamIndex, p0TeamIndices)); + + ProposedBattle memory proposal = ProposedBattle({ + p0: ALICE, + p0TeamIndex: 0, + p0TeamHash: p0TeamHash, + p1: BOB, + p1TeamIndex: 0, + teamRegistry: registry, + validator: IValidator(address(0)), // INLINE VALIDATION + rngOracle: rngOracle, + ruleset: ruleset, + engineHooks: hooks, + moveManager: moveManager, + matchmaker: maker + }); + + vm.startPrank(ALICE); + bytes32 battleKey = maker.proposeBattle(proposal); + + bytes32 battleIntegrityHash = maker.getBattleProposalIntegrityHash(proposal); + vm.startPrank(BOB); + maker.acceptBattle(battleKey, 0, battleIntegrityHash); + + vm.startPrank(ALICE); + maker.confirmBattle(battleKey, salt, p0TeamIndex); + + return battleKey; + } + + // Helper to commit/reveal/execute for a specific engine + function _commitRevealExecuteForEngine( + Engine eng, + DefaultCommitManager cm, + bytes32 battleKey, + uint8 aliceMoveIndex, + uint8 bobMoveIndex, + uint240 aliceExtraData, + uint240 bobExtraData + ) internal { + bytes32 salt = ""; + bytes32 aliceMoveHash = keccak256(abi.encodePacked(aliceMoveIndex, salt, aliceExtraData)); + bytes32 bobMoveHash = keccak256(abi.encodePacked(bobMoveIndex, salt, bobExtraData)); + uint256 turnId = eng.getTurnIdForBattleState(battleKey); + if (turnId % 2 == 0) { + vm.startPrank(ALICE); + cm.commitMove(battleKey, aliceMoveHash); + vm.startPrank(BOB); + cm.revealMove(battleKey, bobMoveIndex, salt, bobExtraData, true); + vm.startPrank(ALICE); + cm.revealMove(battleKey, aliceMoveIndex, salt, aliceExtraData, true); + } else { + vm.startPrank(BOB); + cm.commitMove(battleKey, bobMoveHash); + vm.startPrank(ALICE); + cm.revealMove(battleKey, aliceMoveIndex, salt, aliceExtraData, true); + vm.startPrank(BOB); + cm.revealMove(battleKey, bobMoveIndex, salt, bobExtraData, true); + } + } +} diff --git a/test/InlineValidationTest.sol b/test/InlineValidationTest.sol new file mode 100644 index 00000000..2209cb55 --- /dev/null +++ b/test/InlineValidationTest.sol @@ -0,0 +1,256 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity ^0.8.0; + +import "../lib/forge-std/src/Test.sol"; + +import "../src/Constants.sol"; +import "../src/Enums.sol"; +import "../src/Structs.sol"; + +import {DefaultCommitManager} from "../src/commit-manager/DefaultCommitManager.sol"; +import {Engine} from "../src/Engine.sol"; +import {IEngine} from "../src/IEngine.sol"; +import {IValidator} from "../src/IValidator.sol"; +import {IMoveSet} from "../src/moves/IMoveSet.sol"; +import {MockRandomnessOracle} from "./mocks/MockRandomnessOracle.sol"; +import {TestTeamRegistry} from "./mocks/TestTeamRegistry.sol"; +import {DefaultMatchmaker} from "../src/matchmaker/DefaultMatchmaker.sol"; +import {BattleHelper} from "./abstract/BattleHelper.sol"; +import {TestMoveFactory} from "./mocks/TestMoveFactory.sol"; +/// @title Inline Validation Tests +/// @notice Tests that inline validation works correctly when validator is address(0) +contract InlineValidationTest is Test, BattleHelper { + Engine engine; + DefaultCommitManager commitManager; + MockRandomnessOracle mockOracle; + TestTeamRegistry defaultRegistry; + DefaultMatchmaker matchmaker; + TestMoveFactory moveFactory; + + uint256 constant MONS_PER_TEAM = 2; + uint256 constant MOVES_PER_MON = 1; + + address p0 = address(0x1); + address p1 = address(0x2); + + function setUp() public { + + mockOracle = new MockRandomnessOracle(); + defaultRegistry = new TestTeamRegistry(); + // Create engine with inline validation defaults + engine = new Engine(MONS_PER_TEAM, MOVES_PER_MON, 1); + commitManager = new DefaultCommitManager(engine); + matchmaker = new DefaultMatchmaker(engine); + moveFactory = new TestMoveFactory(IEngine(address(engine))); + + _setupTeams(); + } + + function _setupTeams() internal { + IMoveSet testMove = moveFactory.createMove(MoveClass.Physical, Type.Fire, 10, 10); + + Mon memory mon = _createMon(); + mon.stats.hp = 100; + mon.stats.stamina = 100; + mon.stats.speed = 100; + mon.stats.attack = 100; + mon.stats.defense = 100; + mon.stats.specialAttack = 100; + mon.stats.specialDefense = 100; + mon.moves = new IMoveSet[](MOVES_PER_MON); + mon.moves[0] = testMove; + + Mon[] memory team = new Mon[](MONS_PER_TEAM); + team[0] = mon; + team[1] = mon; + + defaultRegistry.setTeam(p0, team); + defaultRegistry.setTeam(p1, team); + + uint256[] memory indices = new uint256[](MONS_PER_TEAM); + indices[0] = 0; + indices[1] = 1; + defaultRegistry.setIndices(indices); + } + + function _startBattleWithInlineValidation() internal returns (bytes32) { + vm.startPrank(p0); + address[] memory makersToAdd = new address[](1); + makersToAdd[0] = address(matchmaker); + address[] memory makersToRemove = new address[](0); + engine.updateMatchmakers(makersToAdd, makersToRemove); + + vm.startPrank(p1); + engine.updateMatchmakers(makersToAdd, makersToRemove); + + bytes32 salt = ""; + uint96 p0TeamIndex = 0; + uint256[] memory p0TeamIndices = defaultRegistry.getMonRegistryIndicesForTeam(p0, p0TeamIndex); + bytes32 p0TeamHash = keccak256(abi.encodePacked(salt, p0TeamIndex, p0TeamIndices)); + + // Use address(0) as validator for inline validation + ProposedBattle memory proposal = ProposedBattle({ + p0: p0, + p0TeamIndex: 0, + p0TeamHash: p0TeamHash, + p1: p1, + p1TeamIndex: 0, + teamRegistry: defaultRegistry, + validator: IValidator(address(0)), // Inline validation! + rngOracle: mockOracle, + ruleset: IRuleset(address(0)), + engineHooks: new IEngineHook[](0), + moveManager: address(commitManager), + matchmaker: matchmaker + }); + + vm.startPrank(p0); + bytes32 battleKey = matchmaker.proposeBattle(proposal); + + bytes32 battleIntegrityHash = matchmaker.getBattleProposalIntegrityHash(proposal); + vm.startPrank(p1); + matchmaker.acceptBattle(battleKey, 0, battleIntegrityHash); + + vm.startPrank(p0); + matchmaker.confirmBattle(battleKey, salt, p0TeamIndex); + + return battleKey; + } + + /// @notice Test that inline switch validation works correctly + function test_inlineValidation_switchWorks() public { + bytes32 battleKey = _startBattleWithInlineValidation(); + + // Both players switch in mon 0 + bytes32 salt = ""; + bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, salt, uint240(0))); + bytes32 p1MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, salt, uint240(0))); + + vm.startPrank(p0); + commitManager.commitMove(battleKey, p0MoveHash); + + vm.startPrank(p1); + commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, salt, 0, false); + + vm.startPrank(p0); + commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, salt, 0, true); + + // Verify both players have mon 0 active + uint256 p0ActiveMon = engine.getActiveMonIndexForBattleState(battleKey)[0]; + uint256 p1ActiveMon = engine.getActiveMonIndexForBattleState(battleKey)[1]; + assertEq(p0ActiveMon, 0, "P0 should have mon 0 active"); + assertEq(p1ActiveMon, 0, "P1 should have mon 0 active"); + } + + /// @notice Test that inline move validation works correctly + function test_inlineValidation_moveWorks() public { + bytes32 battleKey = _startBattleWithInlineValidation(); + + // Both players switch in mon 0 + bytes32 salt = ""; + bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, salt, uint240(0))); + + vm.startPrank(p0); + commitManager.commitMove(battleKey, p0MoveHash); + vm.startPrank(p1); + commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, salt, 0, false); + vm.startPrank(p0); + commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, salt, 0, true); + + // Now use move 0 (attack) + bytes32 p0AttackHash = keccak256(abi.encodePacked(uint8(0), salt, uint240(0))); + + vm.startPrank(p1); + commitManager.commitMove(battleKey, p0AttackHash); + vm.startPrank(p0); + commitManager.revealMove(battleKey, 0, salt, 0, false); + vm.startPrank(p1); + commitManager.revealMove(battleKey, 0, salt, 0, true); + + // Check that battle advanced (turn should be 2) + uint256 turnId = engine.getTurnIdForBattleState(battleKey); + assertEq(turnId, 2, "Turn should be 2 after switch + attack"); + } + + /// @notice Test that inline validation rejects invalid switch (to already active mon) + function test_inlineValidation_rejectsInvalidSwitch() public { + bytes32 battleKey = _startBattleWithInlineValidation(); + + // Both players switch in mon 0 + bytes32 salt = ""; + bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, salt, uint240(0))); + + vm.startPrank(p0); + commitManager.commitMove(battleKey, p0MoveHash); + vm.startPrank(p1); + commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, salt, 0, false); + vm.startPrank(p0); + commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, salt, 0, true); + + // P1 commits turn 1 - try to switch to mon 0 again (invalid - already active) + // The inline validation should treat this as invalid and fall through + bytes32 p1InvalidSwitchHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, salt, uint240(0))); + + vm.startPrank(p1); + commitManager.commitMove(battleKey, p1InvalidSwitchHash); + + // P0 reveals a valid move + vm.startPrank(p0); + commitManager.revealMove(battleKey, NO_OP_MOVE_INDEX, salt, 0, false); + + // P1 reveals invalid switch - should still execute but switch is ignored + vm.startPrank(p1); + commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, salt, 0, true); + + // P1's active mon should still be 0 (switch was invalid) + uint256 p1ActiveMon = engine.getActiveMonIndexForBattleState(battleKey)[1]; + assertEq(p1ActiveMon, 0, "P1 should still have mon 0 active (invalid switch)"); + } + + /// @notice Test multiple turns with inline validation + function test_inlineValidation_multipleRounds() public { + bytes32 battleKey = _startBattleWithInlineValidation(); + bytes32 salt = ""; + + // Turn 0: Both switch in mon 0 + bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, salt, uint240(0))); + vm.startPrank(p0); + commitManager.commitMove(battleKey, p0MoveHash); + vm.startPrank(p1); + commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, salt, 0, false); + vm.startPrank(p0); + commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, salt, 0, true); + + // Run a few attack rounds to verify inline validation works across multiple turns + for (uint256 i = 0; i < 5; i++) { + // Check if battle ended + (, BattleData memory bd) = engine.getBattle(battleKey); + if (bd.winnerIndex != 2) break; + // Skip if only one player needs to move (KO occurred) + if (bd.playerSwitchForTurnFlag != 2) break; + + uint256 turnId = engine.getTurnIdForBattleState(battleKey); + bytes32 attackHash = keccak256(abi.encodePacked(uint8(0), salt, uint240(0))); + + if (turnId % 2 == 0) { + vm.startPrank(p0); + commitManager.commitMove(battleKey, attackHash); + vm.startPrank(p1); + commitManager.revealMove(battleKey, 0, salt, 0, false); + vm.startPrank(p0); + commitManager.revealMove(battleKey, 0, salt, 0, true); + } else { + vm.startPrank(p1); + commitManager.commitMove(battleKey, attackHash); + vm.startPrank(p0); + commitManager.revealMove(battleKey, 0, salt, 0, false); + vm.startPrank(p1); + commitManager.revealMove(battleKey, 0, salt, 0, true); + } + } + + // Battle should have progressed past turn 1 + uint256 finalTurnId = engine.getTurnIdForBattleState(battleKey); + assertTrue(finalTurnId > 1, "Battle should have progressed past turn 1"); + } +} diff --git a/test/MatchmakerTest.sol b/test/MatchmakerTest.sol index ede79925..a4a7575d 100644 --- a/test/MatchmakerTest.sol +++ b/test/MatchmakerTest.sol @@ -33,7 +33,7 @@ contract MatchmakerTest is Test, BattleHelper { function setUp() public { defaultOracle = new DefaultRandomnessOracle(); - engine = new Engine(); + engine = new Engine(0, 0, 0); commitManager = new DefaultCommitManager(engine); validator = new DefaultValidator( engine, DefaultValidator.Args({MONS_PER_TEAM: 1, MOVES_PER_MON: 0, TIMEOUT_DURATION: TIMEOUT}) diff --git a/test/SignedCommitManager.t.sol b/test/SignedCommitManager.t.sol index 7d8deb62..e353d2a2 100644 --- a/test/SignedCommitManager.t.sol +++ b/test/SignedCommitManager.t.sol @@ -47,7 +47,7 @@ abstract contract SignedCommitManagerTestBase is Test, BattleHelper, EIP712 { mockOracle = new MockRandomnessOracle(); defaultRegistry = new TestTeamRegistry(); - engine = new Engine(); + engine = new Engine(0, 0, 0); validator = new DefaultValidator( IEngine(address(engine)), DefaultValidator.Args({MONS_PER_TEAM: 2, MOVES_PER_MON: 1, TIMEOUT_DURATION: 100}) diff --git a/test/SignedMatchmakerTest.sol b/test/SignedMatchmakerTest.sol index bbe2a22a..dcd52f90 100644 --- a/test/SignedMatchmakerTest.sol +++ b/test/SignedMatchmakerTest.sol @@ -39,7 +39,7 @@ contract SignedMatchmakerTest is Test, BattleHelper { p1 = vm.addr(P1_PK); // Deploy contracts - engine = new Engine(); + engine = new Engine(0, 0, 0); rngOracle = new DefaultRandomnessOracle(); commitManager = new DefaultCommitManager(engine); validator = new DefaultValidator( diff --git a/test/effects/EffectTest.sol b/test/effects/EffectTest.sol index 1e1039e8..43016e3b 100644 --- a/test/effects/EffectTest.sol +++ b/test/effects/EffectTest.sol @@ -77,7 +77,7 @@ contract EffectTest is Test, BattleHelper { */ function setUp() public { mockOracle = new MockRandomnessOracle(); - engine = new Engine(); + engine = new Engine(0, 0, 0); commitManager = new DefaultCommitManager(engine); oneMonOneMoveValidator = new DefaultValidator( engine, DefaultValidator.Args({MONS_PER_TEAM: 1, MOVES_PER_MON: 1, TIMEOUT_DURATION: TIMEOUT_DURATION}) diff --git a/test/effects/StatBoosts.t.sol b/test/effects/StatBoosts.t.sol index bf62c28c..d7677b30 100644 --- a/test/effects/StatBoosts.t.sol +++ b/test/effects/StatBoosts.t.sol @@ -49,7 +49,7 @@ contract StatBoostsTest is Test, BattleHelper { typeCalc = new TestTypeCalculator(); mockOracle = new MockRandomnessOracle(); defaultRegistry = new TestTeamRegistry(); - engine = new Engine(); + engine = new Engine(0, 0, 0); validator = new DefaultValidator( IEngine(address(engine)), DefaultValidator.Args({MONS_PER_TEAM: 2, MOVES_PER_MON: 1, TIMEOUT_DURATION: 10}) ); diff --git a/test/mocks/AfterDamageReboundEffect.sol b/test/mocks/AfterDamageReboundEffect.sol index fa820167..aeb0fa9e 100644 --- a/test/mocks/AfterDamageReboundEffect.sol +++ b/test/mocks/AfterDamageReboundEffect.sol @@ -21,14 +21,14 @@ contract AfterDamageReboundEffect is BasicEffect { } // NOTE: CURRENTLY ONLY RUN LOCALLY ON MONS (global effects do not have this hook) - function onAfterDamage(uint256, bytes32 extraData, uint256 targetIndex, uint256 monIndex, int32) + function onAfterDamage(bytes32 battleKey, uint256, bytes32 extraData, uint256 targetIndex, uint256 monIndex, uint256, uint256, int32) external override returns (bytes32, bool) { // Heals for all damage done int32 currentDamage = - ENGINE.getMonStateForBattle(ENGINE.battleKeyForWrite(), targetIndex, monIndex, MonStateIndexName.Hp); + ENGINE.getMonStateForBattle(battleKey, targetIndex, monIndex, MonStateIndexName.Hp); ENGINE.updateMonState(targetIndex, monIndex, MonStateIndexName.Hp, currentDamage * -1); return (extraData, false); } diff --git a/test/mocks/CustomAttack.sol b/test/mocks/CustomAttack.sol index 49ac1689..aecbca9c 100644 --- a/test/mocks/CustomAttack.sol +++ b/test/mocks/CustomAttack.sol @@ -50,8 +50,15 @@ contract CustomAttack is IMoveSet { return "CustomAttack"; } - function move(bytes32 battleKey, uint256 attackerPlayerIndex, uint240 extraData, uint256 rng) external { - _standardAttack.move(battleKey, attackerPlayerIndex, extraData, rng); + function move( + bytes32 battleKey, + uint256 attackerPlayerIndex, + uint256 attackerMonIndex, + uint256 defenderMonIndex, + uint240 extraData, + uint256 rng + ) external { + _standardAttack.move(battleKey, attackerPlayerIndex, attackerMonIndex, defenderMonIndex, extraData, rng); } function priority(bytes32 battleKey, uint256 playerIndex) external view returns (uint32) { diff --git a/test/mocks/EditEffectAttack.sol b/test/mocks/EditEffectAttack.sol index a637d093..ce5d8fb6 100644 --- a/test/mocks/EditEffectAttack.sol +++ b/test/mocks/EditEffectAttack.sol @@ -18,7 +18,7 @@ contract EditEffectAttack is IMoveSet { return "Edit Effect Attack"; } - function move(bytes32, uint256, uint240 extraData, uint256) external { + function move(bytes32, uint256, uint256, uint256, uint240 extraData, uint256) external { // Unpack extraData: lower 80 bits = targetIndex, next 80 bits = monIndex, upper 80 bits = effectIndex uint256 targetIndex = uint256(extraData) & ((1 << 80) - 1); uint256 monIndex = (uint256(extraData) >> 80) & ((1 << 80) - 1); diff --git a/test/mocks/EffectAttack.sol b/test/mocks/EffectAttack.sol index 7dd37a77..55847927 100644 --- a/test/mocks/EffectAttack.sol +++ b/test/mocks/EffectAttack.sol @@ -35,10 +35,9 @@ contract EffectAttack is IMoveSet { return "Effect Attack"; } - function move(bytes32 battleKey, uint256 attackerPlayerIndex, uint240, uint256) external { + function move(bytes32, uint256 attackerPlayerIndex, uint256, uint256 defenderMonIndex, uint240, uint256) external { uint256 targetIndex = (attackerPlayerIndex + 1) % 2; - uint256 activeMonIndex = ENGINE.getActiveMonIndexForBattleState(battleKey)[targetIndex]; - ENGINE.addEffect(targetIndex, activeMonIndex, EFFECT, bytes32(0)); + ENGINE.addEffect(targetIndex, defenderMonIndex, EFFECT, bytes32(0)); } function priority(bytes32, uint256) external view returns (uint32) { diff --git a/test/mocks/ForceSwitchMove.sol b/test/mocks/ForceSwitchMove.sol index 3f7e460f..f8283a9e 100644 --- a/test/mocks/ForceSwitchMove.sol +++ b/test/mocks/ForceSwitchMove.sol @@ -32,7 +32,7 @@ contract ForceSwitchMove is IMoveSet { return "Force Switch"; } - function move(bytes32, uint256, uint240 extraData, uint256) external { + function move(bytes32, uint256, uint256, uint256, uint240 extraData, uint256) external { // Decode data as packed (playerIndex in lower 120 bits, monToSwitchIndex in upper 120 bits) uint256 playerIndex = uint256(extraData) & ((1 << 120) - 1); uint256 monToSwitchIndex = uint256(extraData) >> 120; diff --git a/test/mocks/GlobalEffectAttack.sol b/test/mocks/GlobalEffectAttack.sol index 99ae254f..2e699747 100644 --- a/test/mocks/GlobalEffectAttack.sol +++ b/test/mocks/GlobalEffectAttack.sol @@ -35,7 +35,7 @@ contract GlobalEffectAttack is IMoveSet { return "Effect Attack"; } - function move(bytes32, uint256, uint240, uint256) external { + function move(bytes32, uint256, uint256, uint256, uint240, uint256) external { ENGINE.addEffect(2, 0, EFFECT, bytes32(0)); } diff --git a/test/mocks/InstantDeathEffect.sol b/test/mocks/InstantDeathEffect.sol index 0b3cabcc..92bd199e 100644 --- a/test/mocks/InstantDeathEffect.sol +++ b/test/mocks/InstantDeathEffect.sol @@ -24,7 +24,7 @@ contract InstantDeathEffect is BasicEffect { return 0x04; } - function onRoundEnd(uint256, bytes32, uint256 targetIndex, uint256 monIndex) + function onRoundEnd(bytes32, uint256, bytes32, uint256 targetIndex, uint256 monIndex, uint256, uint256) external override returns (bytes32 updatedExtraData, bool removeAfterRun) diff --git a/test/mocks/InstantDeathOnSwitchInEffect.sol b/test/mocks/InstantDeathOnSwitchInEffect.sol index f825576d..b1fde835 100644 --- a/test/mocks/InstantDeathOnSwitchInEffect.sol +++ b/test/mocks/InstantDeathOnSwitchInEffect.sol @@ -25,7 +25,7 @@ contract InstantDeathOnSwitchInEffect is BasicEffect { } // NOTE: ONLY RUN ON GLOBAL EFFECTS (mons have their Ability as their own hook to apply an effect on switch in) - function onMonSwitchIn(uint256, bytes32, uint256 targetIndex, uint256 monIndex) + function onMonSwitchIn(bytes32, uint256, bytes32, uint256 targetIndex, uint256 monIndex, uint256, uint256) external override returns (bytes32 updatedExtraData, bool removeAfterRun) diff --git a/test/mocks/InvalidMove.sol b/test/mocks/InvalidMove.sol index cba0e526..9bcb589f 100644 --- a/test/mocks/InvalidMove.sol +++ b/test/mocks/InvalidMove.sol @@ -20,7 +20,7 @@ contract InvalidMove is IMoveSet { return "Effect Attack"; } - function move(bytes32, uint256, uint240, uint256) external pure { + function move(bytes32, uint256, uint256, uint256, uint240, uint256) external pure { // No-op } diff --git a/test/mocks/MockEffectRemover.sol b/test/mocks/MockEffectRemover.sol index f993bbe5..a5620b72 100644 --- a/test/mocks/MockEffectRemover.sol +++ b/test/mocks/MockEffectRemover.sol @@ -24,21 +24,25 @@ contract MockEffectRemover is IMoveSet { return "Mock Effect Remover"; } - function move(bytes32 battleKey, uint256 attackerPlayerIndex, uint240 extraData, uint256) external { + function move( + bytes32 battleKey, + uint256 attackerPlayerIndex, + uint256, + uint256 defenderMonIndex, + uint240 extraData, + uint256 + ) external { // extraData contains the address of the effect to remove (packed as uint160) address effectToRemove = address(uint160(extraData)); // Target the opponent's active mon uint256 targetPlayerIndex = 1 - attackerPlayerIndex; - uint256 targetMonIndex = ENGINE.getActiveMonIndexForBattleState(battleKey)[targetPlayerIndex]; - // Find and remove the effect - (EffectInstance[] memory effects, uint256[] memory indices) = ENGINE.getEffects(battleKey, targetPlayerIndex, targetMonIndex); + // Find and remove the effect (removeEffect in Engine handles calling onRemove with proper params) + (EffectInstance[] memory effects, uint256[] memory indices) = ENGINE.getEffects(battleKey, targetPlayerIndex, defenderMonIndex); for (uint256 i = 0; i < effects.length; i++) { if (address(effects[i].effect) == effectToRemove) { - // Call onRemove on the effect before removing - effects[i].effect.onRemove(effects[i].data, targetPlayerIndex, targetMonIndex); - ENGINE.removeEffect(targetPlayerIndex, targetMonIndex, indices[i]); + ENGINE.removeEffect(targetPlayerIndex, defenderMonIndex, indices[i]); break; } } diff --git a/test/mocks/OnUpdateMonStateHealEffect.sol b/test/mocks/OnUpdateMonStateHealEffect.sol index e7421a5b..ac002cba 100644 --- a/test/mocks/OnUpdateMonStateHealEffect.sol +++ b/test/mocks/OnUpdateMonStateHealEffect.sol @@ -29,10 +29,13 @@ contract OnUpdateMonStateHealEffect is BasicEffect { // WARNING: Avoid chaining this effect to prevent recursive calls // This effect is safe because it only heals HP, it doesn't trigger state updates that would recurse function onUpdateMonState( + bytes32, uint256, bytes32 extraData, uint256 playerIndex, uint256 monIndex, + uint256, + uint256, MonStateIndexName stateVarIndex, int32 valueToAdd ) external override returns (bytes32, bool) { diff --git a/test/mocks/OneTurnStatBoost.sol b/test/mocks/OneTurnStatBoost.sol index e89202cf..41acb9fd 100644 --- a/test/mocks/OneTurnStatBoost.sol +++ b/test/mocks/OneTurnStatBoost.sol @@ -25,7 +25,7 @@ contract OneTurnStatBoost is BasicEffect { } // Adds a bonus - function onApply(uint256, bytes32, uint256 targetIndex, uint256 monIndex) + function onApply(bytes32, uint256, bytes32, uint256 targetIndex, uint256 monIndex, uint256, uint256) external override returns (bytes32 updatedExtraData, bool removeAfterRun) @@ -35,7 +35,7 @@ contract OneTurnStatBoost is BasicEffect { } // Adds another bonus - function onRoundEnd(uint256, bytes32, uint256 targetIndex, uint256 monIndex) + function onRoundEnd(bytes32, uint256, bytes32, uint256 targetIndex, uint256 monIndex, uint256, uint256) external override returns (bytes32 updatedExtraData, bool removeAfterRun) diff --git a/test/mocks/ReduceSpAtkMove.sol b/test/mocks/ReduceSpAtkMove.sol index 2e0f5d9c..abc6d9e1 100644 --- a/test/mocks/ReduceSpAtkMove.sol +++ b/test/mocks/ReduceSpAtkMove.sol @@ -25,15 +25,12 @@ contract ReduceSpAtkMove is IMoveSet { return "Reduce SpAtk"; } - function move(bytes32, uint256 attackerPlayerIndex, uint240, uint256) external { + function move(bytes32, uint256 attackerPlayerIndex, uint256, uint256 defenderMonIndex, uint240, uint256) external { // Get the opposing player's index uint256 opposingPlayerIndex = (attackerPlayerIndex + 1) % 2; - // Get the opposing player's active mon index - uint256 opposingMonIndex = ENGINE.getActiveMonIndexForBattleState(ENGINE.battleKeyForWrite())[opposingPlayerIndex]; - // Reduce the opposing mon's SpecialAttack by 1 - ENGINE.updateMonState(opposingPlayerIndex, opposingMonIndex, MonStateIndexName.SpecialAttack, -1); + ENGINE.updateMonState(opposingPlayerIndex, defenderMonIndex, MonStateIndexName.SpecialAttack, -1); } function priority(bytes32, uint256) external pure returns (uint32) { diff --git a/test/mocks/SelfSwitchAndDamageMove.sol b/test/mocks/SelfSwitchAndDamageMove.sol index e91b48b9..a9bf0301 100644 --- a/test/mocks/SelfSwitchAndDamageMove.sol +++ b/test/mocks/SelfSwitchAndDamageMove.sol @@ -23,14 +23,12 @@ contract SelfSwitchAndDamageMove is IMoveSet { return "Self Switch And Damage Move"; } - function move(bytes32, uint256 attackerPlayerIndex, uint240 extraData, uint256) external { + function move(bytes32, uint256 attackerPlayerIndex, uint256, uint256 defenderMonIndex, uint240 extraData, uint256) external { uint256 monToSwitchIndex = uint256(extraData); // Deal damage first to opponent uint256 otherPlayerIndex = (attackerPlayerIndex + 1) % 2; - uint256 otherPlayerActiveMonIndex = - ENGINE.getActiveMonIndexForBattleState(ENGINE.battleKeyForWrite())[otherPlayerIndex]; - ENGINE.dealDamage(otherPlayerIndex, otherPlayerActiveMonIndex, DAMAGE); + ENGINE.dealDamage(otherPlayerIndex, defenderMonIndex, DAMAGE); // Use the new switchActiveMon function ENGINE.switchActiveMon(attackerPlayerIndex, monToSwitchIndex); diff --git a/test/mocks/SingleInstanceEffect.sol b/test/mocks/SingleInstanceEffect.sol index 42ff355a..77e9ec9c 100644 --- a/test/mocks/SingleInstanceEffect.sol +++ b/test/mocks/SingleInstanceEffect.sol @@ -24,7 +24,7 @@ contract SingleInstanceEffect is BasicEffect { return 0x01; } - function onApply(uint256, bytes32, uint256 targetIndex, uint256 monIndex) + function onApply(bytes32, uint256, bytes32, uint256 targetIndex, uint256 monIndex, uint256, uint256) external override returns (bytes32, bool removeAfterRun) @@ -34,9 +34,9 @@ contract SingleInstanceEffect is BasicEffect { return (bytes32(0), false); } - function shouldApply(bytes32, uint256 targetIndex, uint256 monIndex) external view override returns (bool) { + function shouldApply(bytes32 battleKey, bytes32, uint256 targetIndex, uint256 monIndex) external view override returns (bool) { bytes32 indexHash = keccak256(abi.encode(targetIndex, monIndex)); - uint192 value = ENGINE.getGlobalKV(ENGINE.battleKeyForWrite(), indexHash); + uint192 value = ENGINE.getGlobalKV(battleKey, indexHash); return value == 0; } } diff --git a/test/mocks/SkipTurnMove.sol b/test/mocks/SkipTurnMove.sol index a3be8355..4396c0f9 100644 --- a/test/mocks/SkipTurnMove.sol +++ b/test/mocks/SkipTurnMove.sol @@ -32,10 +32,9 @@ contract SkipTurnMove is IMoveSet { return "Skip Turn"; } - function move(bytes32 battleKey, uint256 attackerPlayerIndex, uint240, uint256) external { + function move(bytes32, uint256 attackerPlayerIndex, uint256, uint256 defenderMonIndex, uint240, uint256) external { uint256 targetIndex = (attackerPlayerIndex + 1) % 2; - uint256 activeMonIndex = ENGINE.getActiveMonIndexForBattleState(battleKey)[targetIndex]; - ENGINE.updateMonState(targetIndex, activeMonIndex, MonStateIndexName.ShouldSkipTurn, 1); + ENGINE.updateMonState(targetIndex, defenderMonIndex, MonStateIndexName.ShouldSkipTurn, 1); } function priority(bytes32, uint256) external view returns (uint32) { diff --git a/test/mocks/SpAtkDebuffEffect.sol b/test/mocks/SpAtkDebuffEffect.sol index c06828b4..bd7b4669 100644 --- a/test/mocks/SpAtkDebuffEffect.sol +++ b/test/mocks/SpAtkDebuffEffect.sol @@ -26,11 +26,22 @@ contract SpAtkDebuffEffect is StatusEffect { return 0x09; } - function onApply(uint256, bytes32 extraData, uint256 targetIndex, uint256 monIndex) + function onApply( + bytes32 battleKey, + uint256 rng, + bytes32 extraData, + uint256 targetIndex, + uint256 monIndex, + uint256 p0ActiveMonIndex, + uint256 p1ActiveMonIndex + ) public override returns (bytes32 updatedExtraData, bool removeAfterRun) { + // Call parent to set status flag + super.onApply(battleKey, rng, extraData, targetIndex, monIndex, p0ActiveMonIndex, p1ActiveMonIndex); + // Reduce special attack by half StatBoostToApply[] memory statBoosts = new StatBoostToApply[](1); statBoosts[0] = StatBoostToApply({ @@ -44,11 +55,17 @@ contract SpAtkDebuffEffect is StatusEffect { return (extraData, false); } - function onRemove(bytes32 data, uint256 targetIndex, uint256 monIndex) public override { - super.onRemove(data, targetIndex, monIndex); + function onRemove( + bytes32 battleKey, + bytes32 data, + uint256 targetIndex, + uint256 monIndex, + uint256 p0ActiveMonIndex, + uint256 p1ActiveMonIndex + ) public override { + super.onRemove(battleKey, data, targetIndex, monIndex, p0ActiveMonIndex, p1ActiveMonIndex); // Reset the special attack reduction STAT_BOOST.removeStatBoosts(targetIndex, monIndex, StatBoostFlag.Perm); } } - diff --git a/test/mocks/StatBoostsMove.sol b/test/mocks/StatBoostsMove.sol index e74ad176..65a47de6 100644 --- a/test/mocks/StatBoostsMove.sol +++ b/test/mocks/StatBoostsMove.sol @@ -24,7 +24,7 @@ contract StatBoostsMove is IMoveSet { return ""; } - function move(bytes32, uint256, uint240 extraData, uint256) external { + function move(bytes32, uint256, uint256, uint256, uint240 extraData, uint256) external { // Unpack extraData: lower 60 bits = playerIndex, next 60 bits = monIndex, next 60 bits = statIndex, upper 60 bits = boostAmount uint256 playerIndex = uint256(extraData) & ((1 << 60) - 1); uint256 monIndex = (uint256(extraData) >> 60) & ((1 << 60) - 1); diff --git a/test/mocks/TempStatBoostEffect.sol b/test/mocks/TempStatBoostEffect.sol index 5e5cd266..bc58acdb 100644 --- a/test/mocks/TempStatBoostEffect.sol +++ b/test/mocks/TempStatBoostEffect.sol @@ -24,7 +24,7 @@ contract TempStatBoostEffect is BasicEffect { return 0x21; } - function onApply(uint256, bytes32, uint256 targetIndex, uint256 monIndex) + function onApply(bytes32, uint256, bytes32, uint256 targetIndex, uint256 monIndex, uint256, uint256) external override returns (bytes32 updatedExtraData, bool removeAfterRun) @@ -33,7 +33,7 @@ contract TempStatBoostEffect is BasicEffect { return (bytes32(0), false); } - function onMonSwitchOut(uint256, bytes32, uint256 targetIndex, uint256 monIndex) + function onMonSwitchOut(bytes32, uint256, bytes32, uint256 targetIndex, uint256 monIndex, uint256, uint256) external override returns (bytes32 updatedExtraData, bool removeAfterRun) diff --git a/test/mocks/TestMoveFactory.sol b/test/mocks/TestMoveFactory.sol index fd9888f7..b0e7a324 100644 --- a/test/mocks/TestMoveFactory.sol +++ b/test/mocks/TestMoveFactory.sol @@ -26,10 +26,9 @@ contract TestMove is IMoveSet { return "Test Move"; } - function move(bytes32, uint256 attackerPlayerIndex, uint240, uint256) external { + function move(bytes32, uint256 attackerPlayerIndex, uint256, uint256 defenderMonIndex, uint240, uint256) external { uint256 opponentIndex = (attackerPlayerIndex + 1) % 2; - uint256 opponentMonIndex = ENGINE.getActiveMonIndexForBattleState(ENGINE.battleKeyForWrite())[opponentIndex]; - ENGINE.dealDamage(opponentIndex, opponentMonIndex, _damage); + ENGINE.dealDamage(opponentIndex, defenderMonIndex, _damage); } function priority(bytes32, uint256) external pure returns (uint32) { diff --git a/test/mons/AuroxTest.sol b/test/mons/AuroxTest.sol index 5d19876d..5b540f50 100644 --- a/test/mons/AuroxTest.sol +++ b/test/mons/AuroxTest.sol @@ -61,7 +61,7 @@ contract AuroxTest is Test, BattleHelper { typeCalc = new TestTypeCalculator(); mockOracle = new MockRandomnessOracle(); defaultRegistry = new TestTeamRegistry(); - engine = new Engine(); + engine = new Engine(0, 0, 0); commitManager = new DefaultCommitManager(IEngine(address(engine))); statBoosts = new StatBoosts(IEngine(address(engine))); matchmaker = new DefaultMatchmaker(engine); diff --git a/test/mons/EkinekiTest.sol b/test/mons/EkinekiTest.sol index 3818ff40..b878e78b 100644 --- a/test/mons/EkinekiTest.sol +++ b/test/mons/EkinekiTest.sol @@ -58,7 +58,7 @@ contract EkinekiTest is Test, BattleHelper { typeCalc = new TestTypeCalculator(); mockOracle = new MockRandomnessOracle(); defaultRegistry = new TestTeamRegistry(); - engine = new Engine(); + engine = new Engine(0, 0, 0); commitManager = new DefaultCommitManager(IEngine(address(engine))); statBoosts = new StatBoosts(IEngine(address(engine))); matchmaker = new DefaultMatchmaker(engine); @@ -133,7 +133,7 @@ contract EkinekiTest is Test, BattleHelper { assertTrue(bobHpDelta < 0, "Bob should have taken damage from Bubble Bop"); // Now do a fresh battle with single hit to compare - Engine engine2 = new Engine(); + Engine engine2 = new Engine(0, 0, 0); DefaultCommitManager commitManager2 = new DefaultCommitManager(IEngine(address(engine2))); DefaultMatchmaker matchmaker2 = new DefaultMatchmaker(engine2); TestTeamRegistry registry2 = new TestTeamRegistry(); diff --git a/test/mons/EmbursaTest.sol b/test/mons/EmbursaTest.sol index 9ab968b1..b5ce8aed 100644 --- a/test/mons/EmbursaTest.sol +++ b/test/mons/EmbursaTest.sol @@ -51,7 +51,7 @@ contract EmbursaTest is Test, BattleHelper { typeCalc = new TestTypeCalculator(); mockOracle = new MockRandomnessOracle(); defaultRegistry = new TestTeamRegistry(); - engine = new Engine(); + engine = new Engine(0, 0, 0); validator = new DefaultValidator( IEngine(address(engine)), DefaultValidator.Args({MONS_PER_TEAM: 2, MOVES_PER_MON: 1, TIMEOUT_DURATION: 10}) ); diff --git a/test/mons/GhouliathTest.sol b/test/mons/GhouliathTest.sol index a01b11d6..ff0282fc 100644 --- a/test/mons/GhouliathTest.sol +++ b/test/mons/GhouliathTest.sol @@ -53,7 +53,7 @@ contract GhouliathTest is Test, BattleHelper { typeCalc = new TestTypeCalculator(); mockOracle = new MockRandomnessOracle(); defaultRegistry = new TestTeamRegistry(); - engine = new Engine(); + engine = new Engine(0, 0, 0); validator = new DefaultValidator( IEngine(address(engine)), DefaultValidator.Args({MONS_PER_TEAM: 2, MOVES_PER_MON: 1, TIMEOUT_DURATION: 10}) ); diff --git a/test/mons/GorillaxTest.sol b/test/mons/GorillaxTest.sol index 2113e295..c2150f44 100644 --- a/test/mons/GorillaxTest.sol +++ b/test/mons/GorillaxTest.sol @@ -40,7 +40,7 @@ contract GorillaxTest is Test, BattleHelper { typeCalc = new TestTypeCalculator(); mockOracle = new MockRandomnessOracle(); defaultRegistry = new TestTeamRegistry(); - engine = new Engine(); + engine = new Engine(0, 0, 0); commitManager = new DefaultCommitManager(IEngine(address(engine))); attackFactory = new StandardAttackFactory(IEngine(address(engine)), ITypeCalculator(address(typeCalc))); matchmaker = new DefaultMatchmaker(engine); diff --git a/test/mons/IblivionTest.sol b/test/mons/IblivionTest.sol index 63fd5ea6..8e608429 100644 --- a/test/mons/IblivionTest.sol +++ b/test/mons/IblivionTest.sol @@ -56,7 +56,7 @@ contract IblivionTest is Test, BattleHelper { typeCalc = new TestTypeCalculator(); mockOracle = new MockRandomnessOracle(); defaultRegistry = new TestTeamRegistry(); - engine = new Engine(); + engine = new Engine(0, 0, 0); validator = new DefaultValidator( IEngine(address(engine)), DefaultValidator.Args({MONS_PER_TEAM: 2, MOVES_PER_MON: 4, TIMEOUT_DURATION: 10}) ); diff --git a/test/mons/InutiaTest.sol b/test/mons/InutiaTest.sol index d2afa788..0df43480 100644 --- a/test/mons/InutiaTest.sol +++ b/test/mons/InutiaTest.sol @@ -44,7 +44,7 @@ contract InutiaTest is Test, BattleHelper { typeCalc = new TestTypeCalculator(); mockOracle = new MockRandomnessOracle(); defaultRegistry = new TestTeamRegistry(); - engine = new Engine(); + engine = new Engine(0, 0, 0); commitManager = new DefaultCommitManager(IEngine(address(engine))); statBoost = new StatBoosts(IEngine(address(engine))); interweaving = new Interweaving(IEngine(address(engine)), statBoost); diff --git a/test/mons/MalalienTest.sol b/test/mons/MalalienTest.sol index 9536e975..cee14e7f 100644 --- a/test/mons/MalalienTest.sol +++ b/test/mons/MalalienTest.sol @@ -46,7 +46,7 @@ contract MalalienTest is Test, BattleHelper { typeCalc = new TestTypeCalculator(); mockOracle = new MockRandomnessOracle(); defaultRegistry = new TestTeamRegistry(); - engine = new Engine(); + engine = new Engine(0, 0, 0); commitManager = new DefaultCommitManager(IEngine(address(engine))); statBoosts = new StatBoosts(engine); actusReus = new ActusReus(IEngine(address(engine)), statBoosts); diff --git a/test/mons/PengymTest.sol b/test/mons/PengymTest.sol index 1ace5a2d..e7bce6ef 100644 --- a/test/mons/PengymTest.sol +++ b/test/mons/PengymTest.sol @@ -55,7 +55,7 @@ contract PengymTest is Test, BattleHelper { typeCalc = new TestTypeCalculator(); mockOracle = new MockRandomnessOracle(); defaultRegistry = new TestTeamRegistry(); - engine = new Engine(); + engine = new Engine(0, 0, 0); validator = new DefaultValidator( IEngine(address(engine)), DefaultValidator.Args({MONS_PER_TEAM: 2, MOVES_PER_MON: 1, TIMEOUT_DURATION: 10}) ); diff --git a/test/mons/SofabbiTest.sol b/test/mons/SofabbiTest.sol index e87181ed..ad0c8994 100644 --- a/test/mons/SofabbiTest.sol +++ b/test/mons/SofabbiTest.sol @@ -43,7 +43,7 @@ contract SofabbiTest is Test, BattleHelper { typeCalc = new TestTypeCalculator(); mockOracle = new MockRandomnessOracle(); defaultRegistry = new TestTeamRegistry(); - engine = new Engine(); + engine = new Engine(0, 0, 0); commitManager = new DefaultCommitManager(IEngine(address(engine))); // Initialize the CarrotHarvest ability diff --git a/test/mons/VolthareTest.sol b/test/mons/VolthareTest.sol index 78587e0d..ea5683e9 100644 --- a/test/mons/VolthareTest.sol +++ b/test/mons/VolthareTest.sol @@ -55,7 +55,7 @@ contract VolthareTest is Test, BattleHelper { typeCalc = new TestTypeCalculator(); mockOracle = new MockRandomnessOracle(); defaultRegistry = new TestTeamRegistry(); - engine = new Engine(); + engine = new Engine(0, 0, 0); validator = new DefaultValidator( IEngine(address(engine)), DefaultValidator.Args({MONS_PER_TEAM: 2, MOVES_PER_MON: 0, TIMEOUT_DURATION: 10}) ); diff --git a/test/mons/XmonTest.sol b/test/mons/XmonTest.sol index d60ce8d9..7266912d 100644 --- a/test/mons/XmonTest.sol +++ b/test/mons/XmonTest.sol @@ -59,7 +59,7 @@ contract XmonTest is Test, BattleHelper { typeCalc = new TestTypeCalculator(); mockOracle = new MockRandomnessOracle(); defaultRegistry = new TestTeamRegistry(); - engine = new Engine(); + engine = new Engine(0, 0, 0); commitManager = new DefaultCommitManager(IEngine(address(engine))); matchmaker = new DefaultMatchmaker(engine); attackFactory = new StandardAttackFactory(IEngine(address(engine)), ITypeCalculator(address(typeCalc))); diff --git a/test/moves/AttackCalculatorTest.sol b/test/moves/AttackCalculatorTest.sol index 8c15865f..d620949f 100644 --- a/test/moves/AttackCalculatorTest.sol +++ b/test/moves/AttackCalculatorTest.sol @@ -33,7 +33,7 @@ contract AttackCalculatorTest is Test, BattleHelper { function setUp() public { // Set up the core components - engine = new Engine(); + engine = new Engine(0, 0, 0); typeCalc = new TypeCalculator(); mockOracle = new MockRandomnessOracle(); commitManager = new DefaultCommitManager(engine); diff --git a/test/moves/StandardAttackFactoryTest.sol b/test/moves/StandardAttackFactoryTest.sol index 72f2d449..9c6c5174 100644 --- a/test/moves/StandardAttackFactoryTest.sol +++ b/test/moves/StandardAttackFactoryTest.sol @@ -17,7 +17,7 @@ contract StandardAttackFactoryTest is Test { bytes32 constant TEST_BATTLE_KEY = bytes32(uint256(1)); function setUp() public { - engine = new Engine(); + engine = new Engine(0, 0, 0); typeCalc = new TypeCalculator(); factory = new StandardAttackFactory(engine, typeCalc); }