diff --git a/contracts/src/example-plugins/DuckBurger/DuckBurgerCounter.js b/contracts/src/example-plugins/DuckBurger/DuckBurgerCounter.js index 04796fb16..905ffe3e5 100644 --- a/contracts/src/example-plugins/DuckBurger/DuckBurgerCounter.js +++ b/contracts/src/example-plugins/DuckBurger/DuckBurgerCounter.js @@ -9,173 +9,172 @@ var numBurger = 0; var gameActive = false; export default async function update(state) { - // uncomment this to browse the state object in browser console - // this will be logged when selecting a unit and then selecting an instance of this building - //logState(state); - - const countBuildings = (buildingsArray, type) => { - return buildingsArray.filter(building => - building.kind?.name?.value.toLowerCase().includes(type) - ).length; - } - - const startGame = () => { - const buildingsArray = state.world?.buildings || []; - - numDuckStart = countBuildings(buildingsArray, "duck"); - numBurgerStart = countBuildings(buildingsArray, "burger"); - - numDuck = 0; - numBurger = 0; - gameActive = true; - } - - const endGame = () => { - const buildingsArray = state.world?.buildings || []; - - const totalDuck = countBuildings(buildingsArray, "duck"); - const totalBurger = countBuildings(buildingsArray, "burger"); - - numDuck = totalDuck - numDuckStart; - numBurger = totalBurger - numBurgerStart; - gameActive = false; - } - - const updateNumDuckBurger = () => { - const buildingsArray = state.world?.buildings || []; - - const totalDuck = countBuildings(buildingsArray, "duck"); - const totalBurger = countBuildings(buildingsArray, "burger"); - - numDuck = totalDuck - numDuckStart; - numBurger = totalBurger - numBurgerStart; - } - - if (gameActive){ - updateNumDuckBurger(); - } - - return { - version: 1, - components: [ - { - id: 'duck-burger-counter', - type: 'building', - content: [ - { - id: 'default', - type: 'inline', - html: ` + // uncomment this to browse the state object in browser console + // this will be logged when selecting a unit and then selecting an instance of this building + //logState(state); + + const countBuildings = (buildingsArray, type) => { + return buildingsArray.filter(building => + building.kind?.name?.value.toLowerCase().includes(type) + ).length; + } + + const startGame = () => { + const buildingsArray = state.world?.buildings || []; + + numDuckStart = countBuildings(buildingsArray, "duck"); + numBurgerStart = countBuildings(buildingsArray, "burger"); + + numDuck = 0; + numBurger = 0; + gameActive = true; + } + + const endGame = () => { + const buildingsArray = state.world?.buildings || []; + + const totalDuck = countBuildings(buildingsArray, "duck"); + const totalBurger = countBuildings(buildingsArray, "burger"); + + numDuck = totalDuck - numDuckStart; + numBurger = totalBurger - numBurgerStart; + gameActive = false; + } + + const updateNumDuckBurger = () => { + const buildingsArray = state.world?.buildings || []; + + const totalDuck = countBuildings(buildingsArray, "duck"); + const totalBurger = countBuildings(buildingsArray, "burger"); + + numDuck = totalDuck - numDuckStart; + numBurger = totalBurger - numBurgerStart; + } + + if (gameActive) { + updateNumDuckBurger(); + } + + return { + version: 1, + components: [ + { + id: 'duck-burger-counter', + type: 'building', + content: [ + { + id: 'default', + type: 'inline', + html: ` 🦆: ${numDuck} 🍔: ${numBurger} - ${ - gameActive - ? `duck burger is live! + ${gameActive + ? `duck burger is live! click "End & Count Score" to see who won` - : `click "Start Game" to play` - } + : `click "Start Game" to play` + } `, - buttons: [ - { - text: 'Start Game', - type: 'action', - action: startGame, - disabled: gameActive, - }, - { - text: 'End Game', - type: 'action', - action: endGame, - disabled: !gameActive, - }, - ], - }, - ], - }, + buttons: [ + { + text: 'Start Game', + type: 'action', + action: startGame, + disabled: gameActive, + }, + { + text: 'End Game', + type: 'action', + action: endGame, + disabled: !gameActive, + }, + ], + }, ], - }; + }, + ], + }; } function getMobileUnit(state) { - return state?.selected?.mobileUnit; + return state?.selected?.mobileUnit; } function getSelectedTile(state) { - const tiles = state?.selected?.tiles || {}; - return tiles && tiles.length === 1 ? tiles[0] : undefined; + const tiles = state?.selected?.tiles || {}; + return tiles && tiles.length === 1 ? tiles[0] : undefined; } function getBuildingOnTile(state, tile) { - return (state?.world?.buildings || []).find((b) => tile && b.location?.tile?.id === tile.id); + return (state?.world?.buildings || []).find((b) => tile && b.location?.tile?.id === tile.id); } // returns an array of items the building expects as input function getRequiredInputItems(building) { - return building?.kind?.inputs || []; + return building?.kind?.inputs || []; } // search through all the bags in the world to find those belonging to this building function getBuildingBags(state, building) { - return building ? (state?.world?.bags || []).filter((bag) => bag.equipee?.node.id === building.id) : []; + return building ? (state?.world?.bags || []).filter((bag) => bag.equipee?.node.id === building.id) : []; } // get building input slots function getInputSlots(state, building) { - // inputs are the bag with key 0 owned by the building - const buildingBags = getBuildingBags(state, building); - const inputBag = buildingBags.find((bag) => bag.equipee.key === 0); + // inputs are the bag with key 0 owned by the building + const buildingBags = getBuildingBags(state, building); + const inputBag = buildingBags.find((bag) => bag.equipee.key === 0); - // slots used for crafting have sequential keys startng with 0 - return inputBag && inputBag.slots.sort((a, b) => a.key - b.key); + // slots used for crafting have sequential keys startng with 0 + return inputBag && inputBag.slots.sort((a, b) => a.key - b.key); } // are the required craft input items in the input slots? function inputsAreCorrect(state, building) { - const requiredInputItems = getRequiredInputItems(building); - const inputSlots = getInputSlots(state, building); - - return ( - inputSlots && - inputSlots.length >= requiredInputItems.length && - requiredInputItems.every( - (requiredItem) => - inputSlots[requiredItem.key].item.id == requiredItem.item.id && - inputSlots[requiredItem.key].balance == requiredItem.balance - ) - ); + const requiredInputItems = getRequiredInputItems(building); + const inputSlots = getInputSlots(state, building); + + return ( + inputSlots && + inputSlots.length >= requiredInputItems.length && + requiredInputItems.every( + (requiredItem) => + inputSlots[requiredItem.key].item.id == requiredItem.item.id && + inputSlots[requiredItem.key].balance == requiredItem.balance + ) + ); } function logState(state) { - console.log('State sent to pluging:', state); + console.log('State sent to pluging:', state); } const friendlyPlayerAddresses = [ - // 0x402462EefC217bf2cf4E6814395E1b61EA4c43F7 + // 0x402462EefC217bf2cf4E6814395E1b61EA4c43F7 ]; function unitIsFriendly(state, selectedBuilding) { - const mobileUnit = getMobileUnit(state); - return ( - unitIsBuildingOwner(mobileUnit, selectedBuilding) || - unitIsBuildingAuthor(mobileUnit, selectedBuilding) || - friendlyPlayerAddresses.some((addr) => unitOwnerConnectedToWallet(state, mobileUnit, addr)) - ); + const mobileUnit = getMobileUnit(state); + return ( + unitIsBuildingOwner(mobileUnit, selectedBuilding) || + unitIsBuildingAuthor(mobileUnit, selectedBuilding) || + friendlyPlayerAddresses.some((addr) => unitOwnerConnectedToWallet(state, mobileUnit, addr)) + ); } function unitIsBuildingOwner(mobileUnit, selectedBuilding) { - //console.log('unit owner id:', mobileUnit?.owner?.id, 'building owner id:', selectedBuilding?.owner?.id); - return mobileUnit?.owner?.id && mobileUnit?.owner?.id === selectedBuilding?.owner?.id; + //console.log('unit owner id:', mobileUnit?.owner?.id, 'building owner id:', selectedBuilding?.owner?.id); + return mobileUnit?.owner?.id && mobileUnit?.owner?.id === selectedBuilding?.owner?.id; } function unitIsBuildingAuthor(mobileUnit, selectedBuilding) { - //console.log('unit owner id:', mobileUnit?.owner?.id, 'building author id:', selectedBuilding?.kind?.owner?.id); - return mobileUnit?.owner?.id && mobileUnit?.owner?.id === selectedBuilding?.kind?.owner?.id; + //console.log('unit owner id:', mobileUnit?.owner?.id, 'building author id:', selectedBuilding?.kind?.owner?.id); + return mobileUnit?.owner?.id && mobileUnit?.owner?.id === selectedBuilding?.kind?.owner?.id; } function unitOwnerConnectedToWallet(state, mobileUnit, walletAddress) { - //console.log('Checking player:', state?.player, 'controls unit', mobileUnit, walletAddress); - return mobileUnit?.owner?.id == state?.player?.id && state?.player?.addr == walletAddress; + //console.log('Checking player:', state?.player, 'controls unit', mobileUnit, walletAddress); + return mobileUnit?.owner?.id == state?.player?.id && state?.player?.addr == walletAddress; } // the source for this code is on github where you can find other example buildings: diff --git a/contracts/src/example-plugins/DuckBurger/DuckBurgerHQ.sol b/contracts/src/example-plugins/DuckBurger/DuckBurgerHQ.sol index 75b20c850..b55efbe25 100644 --- a/contracts/src/example-plugins/DuckBurger/DuckBurgerHQ.sol +++ b/contracts/src/example-plugins/DuckBurger/DuckBurgerHQ.sol @@ -28,20 +28,36 @@ contract DuckBurgerHQ is BuildingKind { // function declerations only used to create signatures for the use payload // these functions do not have their own definitions function join() external {} + function start(bytes24 duckBuildingID, bytes24 burgerBuildingID) external {} + function claim() external {} + function reset() external {} - function use(Game ds, bytes24 buildingInstance, bytes24 actor, bytes calldata payload) public { + function use( + Game ds, + bytes24 buildingInstance, + bytes24 actor, + bytes calldata payload + ) public { State state = GetState(ds); - // decode payload and call one of _join, _start, _claim or _reset if ((bytes4)(payload) == this.join.selector) { _join(ds, state, actor, buildingInstance); } else if ((bytes4)(payload) == this.start.selector) { - (bytes24 duckBuildingID, bytes24 burgerBuildingID) = abi.decode(payload[4:], (bytes24, bytes24)); - _start(ds, state, buildingInstance, duckBuildingID, burgerBuildingID); + (bytes24 duckBuildingID, bytes24 burgerBuildingID) = abi.decode( + payload[4:], + (bytes24, bytes24) + ); + _start( + ds, + state, + buildingInstance, + duckBuildingID, + burgerBuildingID + ); } else if ((bytes4)(payload) == this.claim.selector) { _claim(ds, state, actor, buildingInstance); } else if ((bytes4)(payload) == this.reset.selector) { @@ -50,12 +66,22 @@ contract DuckBurgerHQ is BuildingKind { ds.getDispatcher().dispatch( abi.encodeCall( - Actions.SET_DATA_ON_BUILDING, (buildingInstance, "prizePool", bytes32(uint256(_calculatePool()))) + Actions.SET_DATA_ON_BUILDING, + ( + buildingInstance, + "prizePool", + bytes32(uint256(_calculatePool())) + ) ) ); } - function _join(Game ds, State state, bytes24 unitId, bytes24 buildingId) private { + function _join( + Game ds, + State state, + bytes24 unitId, + bytes24 buildingId + ) private { // check game not in progress bool gameActive = uint256(state.getData(buildingId, "gameActive")) == 1; if (gameActive) { @@ -65,7 +91,9 @@ contract DuckBurgerHQ is BuildingKind { // verify payment has been made // this assumes the Unit has issed an action to transfer the fee in the same transaction batch as the use action // see DuckBurgerHQ.js join function for how this is done - uint64 lastKnownPrizeBalance = uint64(uint256(state.getData(buildingId, "lastKnownPrizeBalance"))); + uint64 lastKnownPrizeBalance = uint64( + uint256(state.getData(buildingId, "lastKnownPrizeBalance")) + ); uint64 currentPrizeBalance = _getPrizeBalance(state, buildingId); if ((currentPrizeBalance - joinFee) < lastKnownPrizeBalance) { revert("Fee not paid"); @@ -76,10 +104,14 @@ contract DuckBurgerHQ is BuildingKind { dispatcher.dispatch( abi.encodeCall( Actions.SET_DATA_ON_BUILDING, - (buildingId, "lastKnownPrizeBalance", bytes32(uint256(currentPrizeBalance))) + ( + buildingId, + "lastKnownPrizeBalance", + bytes32(uint256(currentPrizeBalance)) + ) ) ); - + for (uint256 i = 0; i < teamDuckUnits.length; i++) { if (teamDuckUnits[i] == unitId) revert("Already joined"); } @@ -97,13 +129,36 @@ contract DuckBurgerHQ is BuildingKind { } } - function assignUnitToTeam(Game ds, string memory team, bytes24 unitId, bytes24 buildingId) private { + function assignUnitToTeam( + Game ds, + string memory team, + bytes24 unitId, + bytes24 buildingId + ) private { Dispatcher dispatcher = ds.getDispatcher(); - if (keccak256(abi.encodePacked(team)) == keccak256(abi.encodePacked("duck"))) { - processTeam(dispatcher, buildingId, "teamDuck", teamDuckUnits, unitId); - } else if (keccak256(abi.encodePacked(team)) == keccak256(abi.encodePacked("burger"))) { - processTeam(dispatcher, buildingId, "teamBurger", teamBurgerUnits, unitId); + if ( + keccak256(abi.encodePacked(team)) == + keccak256(abi.encodePacked("duck")) + ) { + processTeam( + dispatcher, + buildingId, + "teamDuck", + teamDuckUnits, + unitId + ); + } else if ( + keccak256(abi.encodePacked(team)) == + keccak256(abi.encodePacked("burger")) + ) { + processTeam( + dispatcher, + buildingId, + "teamBurger", + teamBurgerUnits, + unitId + ); } } @@ -117,63 +172,108 @@ contract DuckBurgerHQ is BuildingKind { dispatcher.dispatch( abi.encodeCall( Actions.SET_DATA_ON_BUILDING, - (buildingId, string(abi.encodePacked(teamPrefix, "Length")), bytes32(uint256(teamUnits.length))) + ( + buildingId, + string(abi.encodePacked(teamPrefix, "Length")), + bytes32(uint256(teamUnits.length)) + ) ) ); - string memory teamUnitIndex = - string(abi.encodePacked(teamPrefix, "Unit_", LibString.toString(uint256(teamUnits.length) - 1))); + string memory teamUnitIndex = string( + abi.encodePacked( + teamPrefix, + "Unit_", + LibString.toString(uint256(teamUnits.length) - 1) + ) + ); - dispatcher.dispatch(abi.encodeCall(Actions.SET_DATA_ON_BUILDING, (buildingId, teamUnitIndex, bytes32(unitId)))); + dispatcher.dispatch( + abi.encodeCall( + Actions.SET_DATA_ON_BUILDING, + (buildingId, teamUnitIndex, bytes32(unitId)) + ) + ); } - function _start(Game ds, State state, bytes24 buildingId, bytes24 duckBuildingID, bytes24 burgerBuildingID) - private - { + function _start( + Game ds, + State state, + bytes24 buildingId, + bytes24 duckBuildingID, + bytes24 burgerBuildingID + ) private { Dispatcher dispatcher = ds.getDispatcher(); // check teams have at least one each - uint256 teamDuckLength = uint256(state.getData(buildingId, "teamDuckLength")); - uint256 teamBurgerLength = uint256(state.getData(buildingId, "teamBurgerLength")); + uint256 teamDuckLength = uint256( + state.getData(buildingId, "teamDuckLength") + ); + uint256 teamBurgerLength = uint256( + state.getData(buildingId, "teamBurgerLength") + ); if (teamDuckLength == 0 || teamBurgerLength == 0) { revert("Can't start, both teams must have at least 1 player"); } // set team buildings dispatcher.dispatch( - abi.encodeCall(Actions.SET_DATA_ON_BUILDING, (buildingId, "buildingKindIdDuck", bytes32(duckBuildingID))) + abi.encodeCall( + Actions.SET_DATA_ON_BUILDING, + (buildingId, "buildingKindIdDuck", bytes32(duckBuildingID)) + ) ); dispatcher.dispatch( abi.encodeCall( - Actions.SET_DATA_ON_BUILDING, (buildingId, "buildingKindIdBurger", bytes32(burgerBuildingID)) + Actions.SET_DATA_ON_BUILDING, + (buildingId, "buildingKindIdBurger", bytes32(burgerBuildingID)) ) ); // todo if the game length is a parameter, we could calculate this from the endBlock dispatcher.dispatch( - abi.encodeCall(Actions.SET_DATA_ON_BUILDING, (buildingId, "startBlock", bytes32(uint256(block.number)))) + abi.encodeCall( + Actions.SET_DATA_ON_BUILDING, + (buildingId, "startBlock", bytes32(uint256(block.number))) + ) ); // set endblock to now plus 1 minute (assuming 2 second blocks) // todo do we take time as a param dispatcher.dispatch( abi.encodeCall( - Actions.SET_DATA_ON_BUILDING, (buildingId, "endBlock", bytes32(uint256(block.number + 1 * 30))) + Actions.SET_DATA_ON_BUILDING, + ( + buildingId, + "endBlock", + bytes32(uint256(block.number + 1 * 30)) + ) ) ); // set start to now dispatcher.dispatch( - abi.encodeCall(Actions.SET_DATA_ON_BUILDING, (buildingId, "startBlock", bytes32(uint256(block.number)))) + abi.encodeCall( + Actions.SET_DATA_ON_BUILDING, + (buildingId, "startBlock", bytes32(uint256(block.number))) + ) ); // gameActive dispatcher.dispatch( - abi.encodeCall(Actions.SET_DATA_ON_BUILDING, (buildingId, "gameActive", bytes32(uint256(1)))) + abi.encodeCall( + Actions.SET_DATA_ON_BUILDING, + (buildingId, "gameActive", bytes32(uint256(1))) + ) ); } - function _claim(Game ds, State state, bytes24 unitId, bytes24 buildingId) private { + function _claim( + Game ds, + State state, + bytes24 unitId, + bytes24 buildingId + ) private { // check game finished { uint256 endBlock = uint256(state.getData(buildingId, "endBlock")); @@ -198,13 +298,19 @@ contract DuckBurgerHQ is BuildingKind { break; } } - require(isDuckTeamMember || isBurgerTeamMember, "Unit did not play or has already claimed"); + require( + isDuckTeamMember || isBurgerTeamMember, + "Unit did not play or has already claimed" + ); // count buildings for each team // NOTE: Scoped to avoid stack being too deep bool isDraw; { - (uint24 duckBuildings, uint24 burgerBuildings) = getBuildingCounts(state, buildingId); + (uint24 duckBuildings, uint24 burgerBuildings) = getBuildingCounts( + state, + buildingId + ); // check unit is in winning team @@ -219,13 +325,23 @@ contract DuckBurgerHQ is BuildingKind { // winner! (or drawer) // \todo this currently assumes even teams Dispatcher dispatcher = ds.getDispatcher(); - _awardPrize(state, dispatcher, buildingId, unitId, isDraw ? joinFee : _calculatePrizeAmount()); + _awardPrize( + state, + dispatcher, + buildingId, + unitId, + isDraw ? joinFee : _calculatePrizeAmount() + ); // remember new prize balance dispatcher.dispatch( abi.encodeCall( Actions.SET_DATA_ON_BUILDING, - (buildingId, "lastKnownPrizeBalance", bytes32(uint256(_getPrizeBalance(state, buildingId)))) + ( + buildingId, + "lastKnownPrizeBalance", + bytes32(uint256(_getPrizeBalance(state, buildingId))) + ) ) ); @@ -237,13 +353,25 @@ contract DuckBurgerHQ is BuildingKind { } } - function _awardPrize(State state, Dispatcher dispatcher, bytes24 buildingId, bytes24 unitId, uint64 prizeAmount) - private - { + function _awardPrize( + State state, + Dispatcher dispatcher, + bytes24 buildingId, + bytes24 unitId, + uint64 prizeAmount + ) private { bytes24 prizeBagId = state.getEquipSlot(buildingId, prizeBagSlot); - (bytes24 prizeItemId, /*uint64 balance*/ ) = state.getItemSlot(prizeBagId, prizeItemSlot); + (bytes24 prizeItemId /*uint64 balance*/, ) = state.getItemSlot( + prizeBagId, + prizeItemSlot + ); - (uint8 destBagSlot, uint8 destItemSlot) = _findValidItemSlot(state, unitId, prizeItemId, prizeAmount); + (uint8 destBagSlot, uint8 destItemSlot) = _findValidItemSlot( + state, + unitId, + prizeItemId, + prizeAmount + ); dispatcher.dispatch( abi.encodeCall( @@ -260,19 +388,29 @@ contract DuckBurgerHQ is BuildingKind { ); } - function _findValidItemSlot(State state, bytes24 unitId, bytes24 itemId, uint64 transferAmount) - private - view - returns (uint8 destBagSlot, uint8 destItemSlot) - { + function _findValidItemSlot( + State state, + bytes24 unitId, + bytes24 itemId, + uint64 transferAmount + ) private view returns (uint8 destBagSlot, uint8 destItemSlot) { for (destBagSlot = 0; destBagSlot < 2; destBagSlot++) { bytes24 destBagId = state.getEquipSlot(unitId, destBagSlot); - require(bytes4(destBagId) == Kind.Bag.selector, "findValidItemSlot(): No bag found at equip slot"); + require( + bytes4(destBagId) == Kind.Bag.selector, + "findValidItemSlot(): No bag found at equip slot" + ); for (destItemSlot = 0; destItemSlot < 4; destItemSlot++) { - (bytes24 destItemId, uint64 destBalance) = state.getItemSlot(destBagId, destItemSlot); - if ((destItemId == bytes24(0) || destItemId == itemId) && destBalance + transferAmount <= 100) { + (bytes24 destItemId, uint64 destBalance) = state.getItemSlot( + destBagId, + destItemSlot + ); + if ( + (destItemId == bytes24(0) || destItemId == itemId) && + destBalance + transferAmount <= 100 + ) { // Found valid slot return (destBagSlot, destItemSlot); } @@ -282,7 +420,10 @@ contract DuckBurgerHQ is BuildingKind { revert("No valid slot for prize claim found"); } - function removeUnitFromArray(bytes24[] storage array, bytes24 unitId) private { + function removeUnitFromArray( + bytes24[] storage array, + bytes24 unitId + ) private { for (uint256 i = 0; i < array.length; i++) { if (array[i] == unitId) { array[i] = array[array.length - 1]; @@ -300,31 +441,67 @@ contract DuckBurgerHQ is BuildingKind { // set state to joining (gameActive ?) dispatcher.dispatch( - abi.encodeCall(Actions.SET_DATA_ON_BUILDING, (buildingId, "startBlock", bytes32(uint256(block.number)))) + abi.encodeCall( + Actions.SET_DATA_ON_BUILDING, + (buildingId, "startBlock", bytes32(uint256(block.number))) + ) ); dispatcher.dispatch( - abi.encodeCall(Actions.SET_DATA_ON_BUILDING, (buildingId, "endBlock", bytes32(uint256(block.number)))) + abi.encodeCall( + Actions.SET_DATA_ON_BUILDING, + (buildingId, "endBlock", bytes32(uint256(block.number))) + ) ); dispatcher.dispatch( - abi.encodeCall(Actions.SET_DATA_ON_BUILDING, (buildingId, "gameActive", bytes32(uint256(0)))) + abi.encodeCall( + Actions.SET_DATA_ON_BUILDING, + (buildingId, "gameActive", bytes32(uint256(0))) + ) ); delete teamDuckUnits; delete teamBurgerUnits; - dispatcher.dispatch(abi.encodeCall(Actions.SET_DATA_ON_BUILDING, (buildingId, "teamBurgerLength", bytes32(0)))); - dispatcher.dispatch(abi.encodeCall(Actions.SET_DATA_ON_BUILDING, (buildingId, "teamDuckLength", bytes32(0)))); dispatcher.dispatch( - abi.encodeCall(Actions.SET_DATA_ON_BUILDING, (buildingId, "lastKnownPrizeBalance", bytes32(0))) + abi.encodeCall( + Actions.SET_DATA_ON_BUILDING, + (buildingId, "teamBurgerLength", bytes32(0)) + ) ); - dispatcher.dispatch(abi.encodeCall(Actions.SET_DATA_ON_BUILDING, (buildingId, "prizePool", bytes32(0)))); dispatcher.dispatch( - abi.encodeCall(Actions.SET_DATA_ON_BUILDING, (buildingId, "buildingKindIdDuck", bytes32(0))) + abi.encodeCall( + Actions.SET_DATA_ON_BUILDING, + (buildingId, "teamDuckLength", bytes32(0)) + ) ); dispatcher.dispatch( - abi.encodeCall(Actions.SET_DATA_ON_BUILDING, (buildingId, "buildingKindIdBurger", bytes32(0))) + abi.encodeCall( + Actions.SET_DATA_ON_BUILDING, + (buildingId, "lastKnownPrizeBalance", bytes32(0)) + ) + ); + dispatcher.dispatch( + abi.encodeCall( + Actions.SET_DATA_ON_BUILDING, + (buildingId, "prizePool", bytes32(0)) + ) + ); + dispatcher.dispatch( + abi.encodeCall( + Actions.SET_DATA_ON_BUILDING, + (buildingId, "buildingKindIdDuck", bytes32(0)) + ) + ); + dispatcher.dispatch( + abi.encodeCall( + Actions.SET_DATA_ON_BUILDING, + (buildingId, "buildingKindIdBurger", bytes32(0)) + ) ); } - function _getPrizeBalance(State state, bytes24 buildingId) internal view returns (uint64) { + function _getPrizeBalance( + State state, + bytes24 buildingId + ) internal view returns (uint64) { bytes24 prizeBag = state.getEquipSlot(buildingId, prizeBagSlot); (, uint64 balance) = state.getItemSlot(prizeBag, prizeItemSlot); return balance; @@ -338,30 +515,48 @@ contract DuckBurgerHQ is BuildingKind { return uint64(teamDuckUnits.length + teamBurgerUnits.length) * joinFee; } - function getBuildingCounts(State state, bytes24 buildingInstance) - public - view - returns (uint24 ducks, uint24 burgers) - { - bytes24 duckBuildingKind = bytes24(state.getData(buildingInstance, "buildingKindIdDuck")); - bytes24 burgerBuildingKind = bytes24(state.getData(buildingInstance, "buildingKindIdBurger")); + function getBuildingCounts( + State state, + bytes24 buildingInstance + ) public view returns (uint24 ducks, uint24 burgers) { + bytes24 duckBuildingKind = bytes24( + state.getData(buildingInstance, "buildingKindIdDuck") + ); + bytes24 burgerBuildingKind = bytes24( + state.getData(buildingInstance, "buildingKindIdBurger") + ); uint256 endBlock = uint256(state.getData(buildingInstance, "endBlock")); - uint256 startBlock = uint256(state.getData(buildingInstance, "startBlock")); + uint256 startBlock = uint256( + state.getData(buildingInstance, "startBlock") + ); bytes24 tile = state.getFixedLocation(buildingInstance); bytes24[99] memory arenaTiles = range5(tile); for (uint256 i = 0; i < arenaTiles.length; i++) { bytes24 arenaBuildingID = Node.Building( - DEFAULT_ZONE, coords(arenaTiles[i])[1], coords(arenaTiles[i])[2], coords(arenaTiles[i])[3] + DEFAULT_ZONE, + coords(arenaTiles[i])[1], + coords(arenaTiles[i])[2], + coords(arenaTiles[i])[3] ); if (state.getBuildingKind(arenaBuildingID) == duckBuildingKind) { - uint64 constructionBlockNum = state.getBuildingConstructionBlockNum(arenaBuildingID); - if (constructionBlockNum >= startBlock && constructionBlockNum <= endBlock) { + uint64 constructionBlockNum = state + .getBuildingConstructionBlockNum(arenaBuildingID); + if ( + constructionBlockNum >= startBlock && + constructionBlockNum <= endBlock + ) { ducks++; } - } else if (state.getBuildingKind(arenaBuildingID) == burgerBuildingKind) { - uint64 constructionBlockNum = state.getBuildingConstructionBlockNum(arenaBuildingID); - if (constructionBlockNum >= startBlock && constructionBlockNum <= endBlock) { + } else if ( + state.getBuildingKind(arenaBuildingID) == burgerBuildingKind + ) { + uint64 constructionBlockNum = state + .getBuildingConstructionBlockNum(arenaBuildingID); + if ( + constructionBlockNum >= startBlock && + constructionBlockNum <= endBlock + ) { burgers++; } } @@ -372,12 +567,18 @@ contract DuckBurgerHQ is BuildingKind { keys = CompoundKeyDecoder.INT16_ARRAY(tile); } - function range5(bytes24 tile) internal pure returns (bytes24[99] memory results) { + function range5( + bytes24 tile + ) internal pure returns (bytes24[99] memory results) { int16 range = 5; int16[4] memory tileCoords = coords(tile); uint256 i = 0; for (int16 q = tileCoords[1] - range; q <= tileCoords[1] + range; q++) { - for (int16 r = tileCoords[2] - range; r <= tileCoords[2] + range; r++) { + for ( + int16 r = tileCoords[2] - range; + r <= tileCoords[2] + range; + r++ + ) { int16 s = -q - r; bytes24 nextTile = Node.Tile(0, q, r, s); if (distance(tile, nextTile) <= uint256(uint16(range))) { @@ -389,12 +590,18 @@ contract DuckBurgerHQ is BuildingKind { return results; } - function distance(bytes24 tileA, bytes24 tileB) internal pure returns (uint256) { + function distance( + bytes24 tileA, + bytes24 tileB + ) internal pure returns (uint256) { int16[4] memory a = CompoundKeyDecoder.INT16_ARRAY(tileA); int16[4] memory b = CompoundKeyDecoder.INT16_ARRAY(tileB); - return uint256( - (abs(int256(a[Q]) - int256(b[Q])) + abs(int256(a[R]) - int256(b[R])) + abs(int256(a[S]) - int256(b[S]))) / 2 - ); + return + uint256( + (abs(int256(a[Q]) - int256(b[Q])) + + abs(int256(a[R]) - int256(b[R])) + + abs(int256(a[S]) - int256(b[S]))) / 2 + ); } function abs(int256 n) internal pure returns (int256) { diff --git a/contracts/src/example-plugins/MOBA/MOBA.js b/contracts/src/example-plugins/MOBA/MOBA.js new file mode 100644 index 000000000..9e257318e --- /dev/null +++ b/contracts/src/example-plugins/MOBA/MOBA.js @@ -0,0 +1,470 @@ +import ds from "downstream"; + +const nullBytes24 = `0x${"00".repeat(24)}`; +const redBuildingTopId = "14"; +const blueBuildingTopId = "14"; + +export default async function update(state) { + // + // Action handler functions + // + console.log('update 0') + + // An action can set a form submit handler which will be called after the action along with the form values + let handleFormSubmit; + + const join = () => { + const mobileUnit = getMobileUnit(state); + + const payload = ds.encodeCall("function join()", []); + + ds.dispatch({ + name: "BUILDING_USE", + args: [selectedBuilding.id, mobileUnit.id, payload], + }); + }; + + // NOTE: Because the 'action' doesn't get passed the form values we are setting a global value to a function that will + const start = () => { + handleFormSubmit = startSubmit; + }; + + const startSubmit = (values) => { + const selectedBuildingKindBaseRed = values["buildingKindIdRed"]; + const selectedBuildTypeBaseBlue = values["buildingKindIdBlue"]; + + // Verify selected buildings are different from each other + if (selectedBuildingKindBaseRed == selectedBuildTypeBaseBlue) { + console.error("Team buildings must be different from each other", { + selectedBuildingKindBaseRed, + selectedBuildTypeBaseBlue, + }); + return; + } + + const mobileUnit = getMobileUnit(state); + const payload = ds.encodeCall( + "function start(bytes24 redBaseID, bytes24 blueBaseID)", + [selectedBuildingKindBaseRed, selectedBuildTypeBaseBlue] + ); + + ds.dispatch({ + name: "BUILDING_USE", + args: [selectedBuilding.id, mobileUnit.id, payload], + }); + }; + + const reset = () => { + const mobileUnit = getMobileUnit(state); + const payload = ds.encodeCall("function reset()", []); + + ds.dispatch({ + name: "BUILDING_USE", + args: [selectedBuilding.id, mobileUnit.id, payload], + }); + }; + + // uncomment this to browse the state object in browser console + // this will be logged when selecting a unit and then selecting an instance of this building + // very spammy for a plugin marked as alwaysActive + // logState(state); + + // \todo + // plugins run for a buildingKind and if marked as alwaysActive in the manifest + // this update will ba called every regardless of whether a building is selected + // so we need to find all HQs on the map and update them each in turn + // + // for now we just update the first we find + const dvbBuildingName = "MOBA"; + const selectedBuilding = state.world?.buildings.find( + (b) => b.kind?.name?.value == dvbBuildingName + ); + + + // early out if we don't have any buildings or state isn't ready + if (!selectedBuilding || !state?.world?.buildings) { + console.log("NO MOBA BUILDING FOUND 8"); + return { + version: 1, + map: [], + components: [ + { + id: "moba", + type: "building", + content: [ + { + id: "default", + type: "inline", + html: "", + buttons: [], + }, + ], + }, + ], + }; + } + + + const { + gameActive, + buildingKindIdRed, + buildingKindIdBlue, + redTeamLength, + blueTeamLength, + } = getHQData(selectedBuilding); + console.log({ + gameActive, + buildingKindIdRed, + buildingKindIdBlue, + redTeamLength, + blueTeamLength, + }) + const localBuildings = range5(state, selectedBuilding); + const redCount = countBuildings(localBuildings, buildingKindIdRed); + const blueCount = countBuildings(localBuildings, buildingKindIdBlue); + console.log({ localBuildings, redCount, blueCount }) + // check current game state: + // - NotStarted : GameActive == false + // - Running : GameActive == true && endBlock < currentBlock + // - GameOver : GameActive == true && endBlock >= currentBlock + + // we build a list of button objects that are rendered in the building UI panel when selected + let buttonList = []; + + // we build an html block which is rendered above the buttons + let htmlBlock = + '
total players: ${redTeamLength + blueTeamLength}
`; + } + + // Show what team the unit is on + const mobileUnit = getMobileUnit(state); + + let isOnTeam = false; + if (mobileUnit) { + let unitTeam = ""; + + for (let i = 0; i < redTeamLength; i++) { + if (mobileUnit.id == getHQTeamUnit(selectedBuilding, "red", i)) { + unitTeam = "🔴"; + break; + } + } + + if (unitTeam === "") { + for (let i = 0; i < blueTeamLength; i++) { + if ( + mobileUnit.id == getHQTeamUnit(selectedBuilding, "blue", i) + ) { + unitTeam = "🔵"; + break; + } + } + } + + if (unitTeam !== "") { + isOnTeam = true; + htmlBlock += ` +You are on team ${unitTeam}
+ `; + } + } + + console.log({ isOnTeam }) + + if (!gameActive) { + + if (!isOnTeam) { + + buttonList.push({ + text: `Join Game`, + type: "action", + action: join, + disabled: !canJoin || isOnTeam, + }); + } else { + // Check reason why game can't start + const waitingForStartCondition = + redTeamLength != blueTeamLength || + redTeamLength + blueTeamLength < 2; + let startConditionMessage = ""; + if (waitingForStartCondition) { + if (redTeamLength + blueTeamLength < 2) { + startConditionMessage = "Waiting for players..."; + } else if (redTeamLength != blueTeamLength) { + startConditionMessage = "Teams must be balanced..."; + } + } + + buttonList.push({ + text: waitingForStartCondition + ? startConditionMessage + : "Start", + type: "action", + action: start, + disabled: !canStart || redTeamLength != blueTeamLength, + }); + } + } + + // Show options to select team buildings + htmlBlock += ` +🔴 Team 🔴
+ ${getBuildingKindSelectHtml( + state, + redBuildingTopId, + "buildingKindIdRed" + )} +🔵 Team 🔵
+ ${getBuildingKindSelectHtml( + state, + blueBuildingTopId, + "buildingKindIdBlue" + )} + `; + + if (gameActive) { + // Display selected team buildings + const buildingKindRed = + state.world.buildingKinds.find((b) => b.id === buildingKindIdRed) || + {}; + const buildingKindBlue = + state.world.buildingKinds.find( + (b) => b.id === buildingKindIdBlue + ) || {}; + htmlBlock += ` +Team 🔴: ${buildingKindRed.name?.value}
+Team 🔵: ${buildingKindBlue.name?.value}
+ + `; + + if (redCount !== blueCount) { + const redWon = redCount > blueCount; + htmlBlock += ` +Team ${redWon ? "RED" : "BLUE"} have won the match!
+${redWon ? "🔴🏆🔴" : "🔵🏆🔵"}
+ `; + } + + + } + // Reset is always offered (requires some trust!) + buttonList.push({ + text: "Reset", + type: "action", + action: reset, + disabled: false, + }); + + console.log({ htmlBlock }) + return { + version: 1, + map: [], + components: [ + { + id: "moba", + type: "building", + content: [ + { + id: "default", + type: "inline", + html: htmlBlock, + submit: (values) => { + if (typeof handleFormSubmit == "function") { + handleFormSubmit(values); + } + }, + buttons: buttonList, + }, + ], + }, + ], + }; +} + +function getHQData(selectedBuilding) { + const gameActive = getDataBool(selectedBuilding, "gameActive"); + // const startBlock = getDataInt(selectedBuilding, "startBlock"); + // const endBlock = getDataInt(selectedBuilding, "endBlock"); + const buildingKindIdRed = getDataBytes24( + selectedBuilding, + "buildingKindIdRed" + ); + const buildingKindIdBlue = getDataBytes24( + selectedBuilding, + "buildingKindIdBlue" + ); + const redTeamLength = getDataInt(selectedBuilding, "redTeamLength"); + const blueTeamLength = getDataInt(selectedBuilding, "blueTeamLength"); + + return { + gameActive, + buildingKindIdRed, + buildingKindIdBlue, + redTeamLength, + blueTeamLength, + }; +} + +function getHQTeamUnit(selectedBuilding, team, index) { + return getDataBytes24(selectedBuilding, `${team}TeamUnit_${index}`); +} + +// search the buildings list ofr the display buildings we're gpoing to use +// for team counts and coutdown + +function getBuildingKindSelectHtml(state, buildingTopId, selectId) { + return ` + + `; +} + +// --- Generic State helper functions + +function getMobileUnit(state) { + return state?.selected?.mobileUnit; +} + +// search through all the bags in the world to find those belonging to this eqipee +// eqipee maybe a building, a mobileUnit or a tile +function getEquipeeBags(state, equipee) { + return equipee + ? (state?.world?.bags || []).filter( + (bag) => bag.equipee?.node.id === equipee.id + ) + : []; +} + +function logState(state) { + console.log("State sent to pluging:", state); +} + +// get an array of buildings withiin 5 tiles of building +function range5(state, building) { + const range = 5; + const tileCoords = getTileCoords(building?.location?.tile?.coords); + let i = 0; + const foundBuildings = []; + for (let q = tileCoords[0] - range; q <= tileCoords[0] + range; q++) { + for ( + let r = tileCoords[1] - range; + r <= tileCoords[1] + range; + r++ + ) { + let s = -q - r; + let nextTile = [q, r, s]; + if (distance(tileCoords, nextTile) <= range) { + state?.world?.buildings.forEach((b) => { + if (!b?.location?.tile?.coords) return; + + const buildingCoords = getTileCoords( + b.location.tile.coords + ); + if ( + buildingCoords[0] == nextTile[0] && + buildingCoords[1] == nextTile[1] && + buildingCoords[2] == nextTile[2] + ) { + foundBuildings[i] = b; + i++; + } + }); + } + } + } + return foundBuildings; +} + +function hexToSignedDecimal(hex) { + if (hex.startsWith("0x")) { + hex = hex.substr(2); + } + + let num = parseInt(hex, 16); + let bits = hex.length * 4; + let maxVal = Math.pow(2, bits); + + // Check if the highest bit is set (negative number) + if (num >= maxVal / 2) { + num -= maxVal; + } + + return num; +} + +function getTileCoords(coords) { + return [ + hexToSignedDecimal(coords[1]), + hexToSignedDecimal(coords[2]), + hexToSignedDecimal(coords[3]), + ]; +} + +function distance(tileCoords, nextTile) { + return Math.max( + Math.abs(tileCoords[0] - nextTile[0]), + Math.abs(tileCoords[1] - nextTile[1]), + Math.abs(tileCoords[2] - nextTile[2]) + ); +} + +// -- Building Data + +function getData(buildingInstance, key) { + return getKVPs(buildingInstance)[key]; +} + +function getDataBool(buildingInstance, key) { + var hexVal = getData(buildingInstance, key); + return typeof hexVal === "string" ? parseInt(hexVal, 16) == 1 : false; +} + +function getDataInt(buildingInstance, key) { + var hexVal = getData(buildingInstance, key); + return typeof hexVal === "string" ? parseInt(hexVal, 16) : 0; +} + +function getDataBytes24(buildingInstance, key) { + var hexVal = getData(buildingInstance, key); + return typeof hexVal === "string" ? hexVal.slice(0, -16) : nullBytes24; +} + +function getKVPs(buildingInstance) { + return buildingInstance.allData.reduce((kvps, data) => { + kvps[data.name] = data.value; + return kvps; + }, {}); +} + +const countBuildings = (buildingsArray, kindID) => { + return buildingsArray.filter((b) => b.kind?.id == kindID).length; +}; + + +// the source for this code is on github where you can find other example buildings: +// https://github.com/playmint/ds/tree/main/contracts/src/example-plugins diff --git a/contracts/src/example-plugins/MOBA/MOBA.sol b/contracts/src/example-plugins/MOBA/MOBA.sol new file mode 100644 index 000000000..730ae49a1 --- /dev/null +++ b/contracts/src/example-plugins/MOBA/MOBA.sol @@ -0,0 +1,323 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {Game} from "cog/IGame.sol"; +import {Dispatcher} from "cog/IDispatcher.sol"; +import {State, CompoundKeyDecoder} from "cog/IState.sol"; +import {Schema, Node, DEFAULT_ZONE, Q, R, S, Kind} from "@ds/schema/Schema.sol"; +import {Actions} from "@ds/actions/Actions.sol"; +import {BuildingKind} from "@ds/ext/BuildingKind.sol"; +import "@ds/utils/LibString.sol"; + +using Schema for State; + +contract MOBA is BuildingKind { + bytes24[] private redTeam; + bytes24[] private blueTeam; + + // function declerations only used to create signatures for the use payload + // these functions do not have their own definitions + function join() external {} + + function start(bytes24 redBaseID, bytes24 blueBaseID) external {} + + function reset() external {} + + function use( + Game ds, + bytes24 buildingInstance, + bytes24 actor, + bytes calldata payload + ) public { + State state = GetState(ds); + + // decode payload and call one of _join, _start, _claim or _reset + if ((bytes4)(payload) == this.join.selector) { + _join(ds, state, actor, buildingInstance); + } else if ((bytes4)(payload) == this.start.selector) { + (bytes24 redBaseID, bytes24 blueBaseId) = abi.decode( + payload[4:], + (bytes24, bytes24) + ); + _start(ds, state, buildingInstance, redBaseID, blueBaseId); + } else if ((bytes4)(payload) == this.reset.selector) { + _reset(ds, buildingInstance); + } + } + + function _join( + Game ds, + State state, + bytes24 unitId, + bytes24 buildingId + ) private { + // check game not in progress + bool gameActive = uint256(state.getData(buildingId, "gameActive")) == 1; + + // bool gameActive = state.getDataBool(buildingId, "gameActive"); + // bool gameActive = state.getDataUint256(buildingId, "gameActive") == 1; + // bool gameActive = uint256(state.getData(buildingId, "gameActive")) == 1; + if (gameActive) { + revert("Can't join while a game is already active"); + } + + for (uint256 i = 0; i < redTeam.length; i++) { + if (redTeam[i] == unitId) revert("Already joined"); + } + for (uint256 i = 0; i < blueTeam.length; i++) { + if (blueTeam[i] == unitId) revert("Already joined"); + } + + // Assign a team + if (redTeam.length <= blueTeam.length) { + redTeam.push(unitId); + assignUnitToTeam(ds, "red", unitId, buildingId); + } else { + blueTeam.push(unitId); + assignUnitToTeam(ds, "blue", unitId, buildingId); + } + } + + function assignUnitToTeam( + Game ds, + string memory team, + bytes24 unitId, + bytes24 buildingId + ) private { + Dispatcher dispatcher = ds.getDispatcher(); + + if ( + keccak256(abi.encodePacked(team)) == + keccak256(abi.encodePacked("red")) + ) { + processTeam(dispatcher, buildingId, "redTeam", redTeam, unitId); + } else if ( + keccak256(abi.encodePacked(team)) == + keccak256(abi.encodePacked("blue")) + ) { + processTeam(dispatcher, buildingId, "blueTeam", blueTeam, unitId); + } else { + revert("invalid team"); + } + } + + function processTeam( + Dispatcher dispatcher, + bytes24 buildingId, + string memory teamPrefix, + bytes24[] storage teamUnits, + bytes24 unitId + ) private { + dispatcher.dispatch( + abi.encodeCall( + Actions.SET_DATA_ON_BUILDING, + ( + buildingId, + string(abi.encodePacked(teamPrefix, "Length")), + bytes32(uint256(teamUnits.length)) + ) + ) + ); + + string memory teamUnitIndex = string( + abi.encodePacked( + teamPrefix, + "Unit_", + LibString.toString(uint256(teamUnits.length) - 1) + ) + ); + + dispatcher.dispatch( + abi.encodeCall( + Actions.SET_DATA_ON_BUILDING, + (buildingId, teamUnitIndex, bytes32(unitId)) + ) + ); + } + + function _start( + Game ds, + State state, + bytes24 buildingId, + bytes24 redBaseID, + bytes24 blueBaseID + ) private { + Dispatcher dispatcher = ds.getDispatcher(); + + // check teams have at least one each + uint256 redTeamLength = uint256( + state.getData(buildingId, "redTeamLength") + ); + uint256 blueTeamLength = uint256( + state.getData(buildingId, "blueTeamLength") + ); + if (redTeamLength == 0 || blueTeamLength == 0) { + revert("Can't start, both teams must have at least 1 player"); + } + + require(redBaseID != blueBaseID, "Bases must be different"); + + // set team buildings + dispatcher.dispatch( + abi.encodeCall( + Actions.SET_DATA_ON_BUILDING, + (buildingId, "buildingKindIdRed", bytes32(redBaseID)) + ) + ); + dispatcher.dispatch( + abi.encodeCall( + Actions.SET_DATA_ON_BUILDING, + (buildingId, "buildingKindIdBlue", bytes32(blueBaseID)) + ) + ); + + { + (uint24 redBuildings, uint24 blueBuildings) = getBuildingCounts( + state, + buildingId + ); + + require(redBuildings == 1, "Red team needs a base to start"); + require(blueBuildings == 1, "Blue team needs a base to start"); + } + + // gameActive + dispatcher.dispatch( + abi.encodeCall( + Actions.SET_DATA_ON_BUILDING, + (buildingId, "gameActive", bytes32(uint256(1))) + ) + ); + } + + function removeUnitFromArray( + bytes24[] storage array, + bytes24 unitId + ) private { + for (uint256 i = 0; i < array.length; i++) { + if (array[i] == unitId) { + array[i] = array[array.length - 1]; + array.pop(); + break; + } + } + } + + function _reset(Game ds, bytes24 buildingId) private { + Dispatcher dispatcher = ds.getDispatcher(); + + // todo - do we check if all claims have been made ? + // for now allwing reset any time which requires some trust :) + + dispatcher.dispatch( + abi.encodeCall( + Actions.SET_DATA_ON_BUILDING, + (buildingId, "gameActive", bytes32(uint256(0))) + ) + ); + delete redTeam; + delete blueTeam; + dispatcher.dispatch( + abi.encodeCall( + Actions.SET_DATA_ON_BUILDING, + (buildingId, "blueTeamLength", bytes32(0)) + ) + ); + dispatcher.dispatch( + abi.encodeCall( + Actions.SET_DATA_ON_BUILDING, + (buildingId, "redTeamLength", bytes32(0)) + ) + ); + dispatcher.dispatch( + abi.encodeCall( + Actions.SET_DATA_ON_BUILDING, + (buildingId, "buildingKindIdRed", bytes32(0)) + ) + ); + dispatcher.dispatch( + abi.encodeCall( + Actions.SET_DATA_ON_BUILDING, + (buildingId, "buildingKindIdBlue", bytes32(0)) + ) + ); + } + + function getBuildingCounts( + State state, + bytes24 buildingInstance + ) public view returns (uint24 reds, uint24 blues) { + bytes24 redBuildingKind = bytes24( + state.getData(buildingInstance, "buildingKindIdRed") + ); + bytes24 blueBuildingKind = bytes24( + state.getData(buildingInstance, "buildingKindIdBlue") + ); + bytes24 tile = state.getFixedLocation(buildingInstance); + bytes24[99] memory arenaTiles = range5(tile); + for (uint256 i = 0; i < arenaTiles.length; i++) { + bytes24 arenaBuildingID = Node.Building( + DEFAULT_ZONE, + coords(arenaTiles[i])[1], + coords(arenaTiles[i])[2], + coords(arenaTiles[i])[3] + ); + if (state.getBuildingKind(arenaBuildingID) == redBuildingKind) { + reds++; + } else if ( + state.getBuildingKind(arenaBuildingID) == blueBuildingKind + ) { + blues++; + } + } + } + + function coords(bytes24 tile) internal pure returns (int16[4] memory keys) { + keys = CompoundKeyDecoder.INT16_ARRAY(tile); + } + + function range5( + bytes24 tile + ) internal pure returns (bytes24[99] memory results) { + int16 range = 5; + int16[4] memory tileCoords = coords(tile); + uint256 i = 0; + for (int16 q = tileCoords[1] - range; q <= tileCoords[1] + range; q++) { + for ( + int16 r = tileCoords[2] - range; + r <= tileCoords[2] + range; + r++ + ) { + int16 s = -q - r; + bytes24 nextTile = Node.Tile(0, q, r, s); + if (distance(tile, nextTile) <= uint256(uint16(range))) { + results[i] = nextTile; + i++; + } + } + } + return results; + } + + function distance( + bytes24 tileA, + bytes24 tileB + ) internal pure returns (uint256) { + int16[4] memory a = CompoundKeyDecoder.INT16_ARRAY(tileA); + int16[4] memory b = CompoundKeyDecoder.INT16_ARRAY(tileB); + return + uint256( + (abs(int256(a[Q]) - int256(b[Q])) + + abs(int256(a[R]) - int256(b[R])) + + abs(int256(a[S]) - int256(b[S]))) / 2 + ); + } + + function abs(int256 n) internal pure returns (int256) { + return n >= 0 ? n : -n; + } + + function GetState(Game ds) internal returns (State) { + return ds.getState(); + } +} diff --git a/contracts/src/example-plugins/MOBA/MOBA.yaml b/contracts/src/example-plugins/MOBA/MOBA.yaml new file mode 100644 index 000000000..3a1266264 --- /dev/null +++ b/contracts/src/example-plugins/MOBA/MOBA.yaml @@ -0,0 +1,19 @@ +kind: BuildingKind +spec: + name: MOBA + description: "Play MOBA" + category: custom + model: 11-03 + color: 1 + contract: + file: ./MOBA.sol + plugin: + file: ./MOBA.js + alwaysActive: true + materials: + - name: Green Goo + quantity: 10 + - name: Blue Goo + quantity: 10 + - name: Red Goo + quantity: 10 diff --git a/contracts/src/example-plugins/MOBA/MOBACounter.js b/contracts/src/example-plugins/MOBA/MOBACounter.js new file mode 100644 index 000000000..905ffe3e5 --- /dev/null +++ b/contracts/src/example-plugins/MOBA/MOBACounter.js @@ -0,0 +1,181 @@ +import ds from 'downstream'; + +var numDuckStart = 0; +var numBurgerStart = 0; + +var numDuck = 0; +var numBurger = 0; + +var gameActive = false; + +export default async function update(state) { + // uncomment this to browse the state object in browser console + // this will be logged when selecting a unit and then selecting an instance of this building + //logState(state); + + const countBuildings = (buildingsArray, type) => { + return buildingsArray.filter(building => + building.kind?.name?.value.toLowerCase().includes(type) + ).length; + } + + const startGame = () => { + const buildingsArray = state.world?.buildings || []; + + numDuckStart = countBuildings(buildingsArray, "duck"); + numBurgerStart = countBuildings(buildingsArray, "burger"); + + numDuck = 0; + numBurger = 0; + gameActive = true; + } + + const endGame = () => { + const buildingsArray = state.world?.buildings || []; + + const totalDuck = countBuildings(buildingsArray, "duck"); + const totalBurger = countBuildings(buildingsArray, "burger"); + + numDuck = totalDuck - numDuckStart; + numBurger = totalBurger - numBurgerStart; + gameActive = false; + } + + const updateNumDuckBurger = () => { + const buildingsArray = state.world?.buildings || []; + + const totalDuck = countBuildings(buildingsArray, "duck"); + const totalBurger = countBuildings(buildingsArray, "burger"); + + numDuck = totalDuck - numDuckStart; + numBurger = totalBurger - numBurgerStart; + } + + if (gameActive) { + updateNumDuckBurger(); + } + + return { + version: 1, + components: [ + { + id: 'duck-burger-counter', + type: 'building', + content: [ + { + id: 'default', + type: 'inline', + html: ` + 🦆: ${numDuck} + 🍔: ${numBurger} + ${gameActive + ? `duck burger is live! + click "End & Count Score" to see who won` + : `click "Start Game" to play` + } + `, + + buttons: [ + { + text: 'Start Game', + type: 'action', + action: startGame, + disabled: gameActive, + }, + { + text: 'End Game', + type: 'action', + action: endGame, + disabled: !gameActive, + }, + ], + }, + ], + }, + ], + }; +} + +function getMobileUnit(state) { + return state?.selected?.mobileUnit; +} + +function getSelectedTile(state) { + const tiles = state?.selected?.tiles || {}; + return tiles && tiles.length === 1 ? tiles[0] : undefined; +} + +function getBuildingOnTile(state, tile) { + return (state?.world?.buildings || []).find((b) => tile && b.location?.tile?.id === tile.id); +} + +// returns an array of items the building expects as input +function getRequiredInputItems(building) { + return building?.kind?.inputs || []; +} + +// search through all the bags in the world to find those belonging to this building +function getBuildingBags(state, building) { + return building ? (state?.world?.bags || []).filter((bag) => bag.equipee?.node.id === building.id) : []; +} + +// get building input slots +function getInputSlots(state, building) { + // inputs are the bag with key 0 owned by the building + const buildingBags = getBuildingBags(state, building); + const inputBag = buildingBags.find((bag) => bag.equipee.key === 0); + + // slots used for crafting have sequential keys startng with 0 + return inputBag && inputBag.slots.sort((a, b) => a.key - b.key); +} + +// are the required craft input items in the input slots? +function inputsAreCorrect(state, building) { + const requiredInputItems = getRequiredInputItems(building); + const inputSlots = getInputSlots(state, building); + + return ( + inputSlots && + inputSlots.length >= requiredInputItems.length && + requiredInputItems.every( + (requiredItem) => + inputSlots[requiredItem.key].item.id == requiredItem.item.id && + inputSlots[requiredItem.key].balance == requiredItem.balance + ) + ); +} + +function logState(state) { + console.log('State sent to pluging:', state); +} + +const friendlyPlayerAddresses = [ + // 0x402462EefC217bf2cf4E6814395E1b61EA4c43F7 +]; + +function unitIsFriendly(state, selectedBuilding) { + const mobileUnit = getMobileUnit(state); + return ( + unitIsBuildingOwner(mobileUnit, selectedBuilding) || + unitIsBuildingAuthor(mobileUnit, selectedBuilding) || + friendlyPlayerAddresses.some((addr) => unitOwnerConnectedToWallet(state, mobileUnit, addr)) + ); +} + +function unitIsBuildingOwner(mobileUnit, selectedBuilding) { + //console.log('unit owner id:', mobileUnit?.owner?.id, 'building owner id:', selectedBuilding?.owner?.id); + return mobileUnit?.owner?.id && mobileUnit?.owner?.id === selectedBuilding?.owner?.id; +} + +function unitIsBuildingAuthor(mobileUnit, selectedBuilding) { + //console.log('unit owner id:', mobileUnit?.owner?.id, 'building author id:', selectedBuilding?.kind?.owner?.id); + return mobileUnit?.owner?.id && mobileUnit?.owner?.id === selectedBuilding?.kind?.owner?.id; +} + +function unitOwnerConnectedToWallet(state, mobileUnit, walletAddress) { + //console.log('Checking player:', state?.player, 'controls unit', mobileUnit, walletAddress); + return mobileUnit?.owner?.id == state?.player?.id && state?.player?.addr == walletAddress; +} + +// the source for this code is on github where you can find other example buildings: +// https://github.com/playmint/ds/tree/main/contracts/src/example-plugins diff --git a/contracts/src/schema/Schema.sol b/contracts/src/schema/Schema.sol index 177aea2a3..9870c3bb0 100644 --- a/contracts/src/schema/Schema.sol +++ b/contracts/src/schema/Schema.sol @@ -6,41 +6,73 @@ import {BiomeKind} from "@ds/actions/Actions.sol"; interface Rel { function Owner() external; + function Location() external; + function Biome() external; + function Balance() external; + function Equip() external; + function Is() external; + function Supports() external; + function Implementation() external; + function Material() external; + function Input() external; + function Output() external; + function Has() external; + function Combat() external; + function IsFinalised() external; + function HasTask() external; + function HasQuest() external; + function ID() external; + function HasBlockNum() external; } interface Kind { function ClientPlugin() external; + function Extension() external; + function Player() external; + function MobileUnit() external; + function Bag() external; + function Tile() external; + function BuildingKind() external; + function Building() external; + function Atom() external; + function Item() external; + function CombatSession() external; + function Hash() external; + function BlockNum() external; + function Quest() external; + function Task() external; + function ID() external; } @@ -88,7 +120,11 @@ int16 constant DEFAULT_ZONE = 0; library Node { function ClientPlugin(uint160 id) internal pure returns (bytes24) { - return CompoundKeyEncoder.BYTES(Kind.ClientPlugin.selector, bytes20(uint160(id))); + return + CompoundKeyEncoder.BYTES( + Kind.ClientPlugin.selector, + bytes20(uint160(id)) + ); } function MobileUnit(uint64 id) internal pure returns (bytes24) { @@ -99,48 +135,94 @@ library Node { return CompoundKeyEncoder.UINT64(Kind.Bag.selector, id); } - function Tile(int16 zone, int16 q, int16 r, int16 s) internal pure returns (bytes24) { + function Tile( + int16 zone, + int16 q, + int16 r, + int16 s + ) internal pure returns (bytes24) { require((q + r + s) == 0, "InvalidTileCoords"); - return CompoundKeyEncoder.INT16_ARRAY(Kind.Tile.selector, [zone, q, r, s]); + return + CompoundKeyEncoder.INT16_ARRAY(Kind.Tile.selector, [zone, q, r, s]); } - function Item(string memory name, uint32[3] memory atoms, bool isStackable) internal pure returns (bytes24) { - uint32 uniqueID = uint32(uint256(keccak256(abi.encode(name, atoms, isStackable)))); + function Item( + string memory name, + uint32[3] memory atoms, + bool isStackable + ) internal pure returns (bytes24) { + uint32 uniqueID = uint32( + uint256(keccak256(abi.encode(name, atoms, isStackable))) + ); return Item(uniqueID, atoms, isStackable); } - function Item(uint32 uniqueID, uint32[3] memory atoms, bool isStackable) internal pure returns (bytes24) { + function Item( + uint32 uniqueID, + uint32[3] memory atoms, + bool isStackable + ) internal pure returns (bytes24) { uint32 stackable = 0; if (isStackable) { stackable = 1; } - return bytes24( - abi.encodePacked(Kind.Item.selector, uniqueID, stackable, atoms[GOO_GREEN], atoms[GOO_BLUE], atoms[GOO_RED]) - ); + return + bytes24( + abi.encodePacked( + Kind.Item.selector, + uniqueID, + stackable, + atoms[GOO_GREEN], + atoms[GOO_BLUE], + atoms[GOO_RED] + ) + ); } function Player(address addr) internal pure returns (bytes24) { return CompoundKeyEncoder.ADDRESS(Kind.Player.selector, addr); } - function BuildingKind(uint64 id, BuildingCategory category) internal pure returns (bytes24) { - return CompoundKeyEncoder.BYTES( - Kind.BuildingKind.selector, bytes20(abi.encodePacked(uint32(0), id, uint64(category))) - ); + function BuildingKind( + uint64 id, + BuildingCategory category + ) internal pure returns (bytes24) { + return + CompoundKeyEncoder.BYTES( + Kind.BuildingKind.selector, + bytes20(abi.encodePacked(uint32(0), id, uint64(category))) + ); } function BuildingKind(uint64 id) internal pure returns (bytes24) { - return CompoundKeyEncoder.BYTES( - Kind.BuildingKind.selector, bytes20(abi.encodePacked(uint32(0), id, uint64(BuildingCategory.NONE))) - ); + return + CompoundKeyEncoder.BYTES( + Kind.BuildingKind.selector, + bytes20( + abi.encodePacked( + uint32(0), + id, + uint64(BuildingCategory.NONE) + ) + ) + ); } function Extension(address addr) internal pure returns (bytes24) { return CompoundKeyEncoder.ADDRESS(Kind.Extension.selector, addr); } - function Building(int16 zone, int16 q, int16 r, int16 s) internal pure returns (bytes24) { - return CompoundKeyEncoder.INT16_ARRAY(Kind.Building.selector, [zone, q, r, s]); + function Building( + int16 zone, + int16 q, + int16 r, + int16 s + ) internal pure returns (bytes24) { + return + CompoundKeyEncoder.INT16_ARRAY( + Kind.Building.selector, + [zone, q, r, s] + ); } function CombatSession(uint64 id) internal pure returns (bytes24) { @@ -155,8 +237,17 @@ library Node { return CompoundKeyEncoder.BYTES(Kind.Hash.selector, id); } - function RewardBag(bytes24 sessionID, bytes24 entityID) internal pure returns (bytes24) { - return Node.Bag(uint64(uint16(uint192(sessionID) & type(uint16).max) | (uint48(uint192(entityID)) << 16))); + function RewardBag( + bytes24 sessionID, + bytes24 entityID + ) internal pure returns (bytes24) { + return + Node.Bag( + uint64( + uint16(uint192(sessionID) & type(uint16).max) | + (uint48(uint192(entityID)) << 16) + ) + ); } function Atom(uint64 atomType) internal pure returns (bytes24) { @@ -167,16 +258,35 @@ library Node { return bytes24(Kind.BlockNum.selector); } - function Task(uint32 id, string memory kind) internal pure returns (bytes24) { + function Task( + uint32 id, + string memory kind + ) internal pure returns (bytes24) { uint32 kindHash = uint32(uint256(keccak256(abi.encode(kind)))); - return CompoundKeyEncoder.BYTES( - Kind.Task.selector, bytes20(abi.encodePacked(uint32(0), uint32(0), uint32(0), kindHash, id)) - ); + return + CompoundKeyEncoder.BYTES( + Kind.Task.selector, + bytes20( + abi.encodePacked( + uint32(0), + uint32(0), + uint32(0), + kindHash, + id + ) + ) + ); } function Quest(string memory name) internal pure returns (bytes24) { - uint64 id = uint64(uint256(keccak256(abi.encodePacked("quest/", name)))); - return CompoundKeyEncoder.BYTES(Kind.Quest.selector, bytes20(abi.encodePacked(uint32(0), uint64(0), id))); + uint64 id = uint64( + uint256(keccak256(abi.encodePacked("quest/", name))) + ); + return + CompoundKeyEncoder.BYTES( + Kind.Quest.selector, + bytes20(abi.encodePacked(uint32(0), uint64(0), id)) + ); } } @@ -188,34 +298,94 @@ int16 constant TRAVEL_SPEED = 10; // 10 == 1 tile per block using Schema for State; library Schema { - function setFixedLocation(State state, bytes24 node, bytes24 tile) internal { - return state.set(Rel.Location.selector, uint8(LocationKey.FIXED), node, tile, 0); - } - - function setNextLocation(State state, bytes24 node, bytes24 tile, uint64 arrivalTime) internal { - return state.set(Rel.Location.selector, uint8(LocationKey.NEXT), node, tile, arrivalTime); - } - - function setPrevLocation(State state, bytes24 node, bytes24 tile, uint64 departureTime) internal { - return state.set(Rel.Location.selector, uint8(LocationKey.PREV), node, tile, departureTime); - } - - function getFixedLocation(State state, bytes24 node) internal view returns (bytes24) { - (bytes24 tile,) = state.get(Rel.Location.selector, uint8(LocationKey.FIXED), node); + function setFixedLocation( + State state, + bytes24 node, + bytes24 tile + ) internal { + return + state.set( + Rel.Location.selector, + uint8(LocationKey.FIXED), + node, + tile, + 0 + ); + } + + function setNextLocation( + State state, + bytes24 node, + bytes24 tile, + uint64 arrivalTime + ) internal { + return + state.set( + Rel.Location.selector, + uint8(LocationKey.NEXT), + node, + tile, + arrivalTime + ); + } + + function setPrevLocation( + State state, + bytes24 node, + bytes24 tile, + uint64 departureTime + ) internal { + return + state.set( + Rel.Location.selector, + uint8(LocationKey.PREV), + node, + tile, + departureTime + ); + } + + function getFixedLocation( + State state, + bytes24 node + ) internal view returns (bytes24) { + (bytes24 tile, ) = state.get( + Rel.Location.selector, + uint8(LocationKey.FIXED), + node + ); return tile; } - function getNextLocation(State state, bytes24 node) internal view returns (bytes24) { - (bytes24 tile,) = state.get(Rel.Location.selector, uint8(LocationKey.NEXT), node); + function getNextLocation( + State state, + bytes24 node + ) internal view returns (bytes24) { + (bytes24 tile, ) = state.get( + Rel.Location.selector, + uint8(LocationKey.NEXT), + node + ); return tile; } - function getPrevLocation(State state, bytes24 node) internal view returns (bytes24) { - (bytes24 tile,) = state.get(Rel.Location.selector, uint8(LocationKey.PREV), node); + function getPrevLocation( + State state, + bytes24 node + ) internal view returns (bytes24) { + (bytes24 tile, ) = state.get( + Rel.Location.selector, + uint8(LocationKey.PREV), + node + ); return tile; } - function getCurrentLocation(State state, bytes24 node, uint64 /*atTime*/ ) internal view returns (bytes24) { + function getCurrentLocation( + State state, + bytes24 node, + uint64 /*atTime*/ + ) internal view returns (bytes24) { // ---------- TEMP HACK UNTIL CLIENT CAN HANDLE CALC OF CURRENT LOCATION PROPERLY ------------ return state.getNextLocation(node); // ---------- END HACK ---------------- @@ -276,7 +446,10 @@ library Schema { return state.set(Rel.Biome.selector, 0x0, node, 0x0, uint64(biome)); } - function getBiome(State state, bytes24 node) internal view returns (BiomeKind) { + function getBiome( + State state, + bytes24 node + ) internal view returns (BiomeKind) { (, uint160 biome) = state.get(Rel.Biome.selector, 0x0, node); return BiomeKind(uint8(biome)); } @@ -285,19 +458,31 @@ library Schema { return state.set(Rel.Owner.selector, 0x0, node, ownerNode, 0); } - function getOwner(State state, bytes24 node) internal view returns (bytes24) { - (bytes24 owner,) = state.get(Rel.Owner.selector, 0x0, node); + function getOwner( + State state, + bytes24 node + ) internal view returns (bytes24) { + (bytes24 owner, ) = state.get(Rel.Owner.selector, 0x0, node); return owner; } - function getOwnerAddress(State state, bytes24 ownerNode) internal view returns (address) { + function getOwnerAddress( + State state, + bytes24 ownerNode + ) internal view returns (address) { while (bytes4(ownerNode) != Kind.Player.selector) { ownerNode = state.getOwner(ownerNode); } return address(uint160(uint192(ownerNode))); } - function setItemSlot(State state, bytes24 bag, uint8 slot, bytes24 resource, uint64 balance) internal { + function setItemSlot( + State state, + bytes24 bag, + uint8 slot, + bytes24 resource, + uint64 balance + ) internal { return state.set(Rel.Balance.selector, slot, bag, resource, balance); } @@ -305,55 +490,90 @@ library Schema { return state.remove(Rel.Balance.selector, slot, bag); } - function getItemSlot(State state, bytes24 bag, uint8 slot) - internal - view - returns (bytes24 resource, uint64 balance) - { + function getItemSlot( + State state, + bytes24 bag, + uint8 slot + ) internal view returns (bytes24 resource, uint64 balance) { return state.get(Rel.Balance.selector, slot, bag); } - function setEquipSlot(State state, bytes24 equipee, uint8 equipSlot, bytes24 equipment) internal { + function setEquipSlot( + State state, + bytes24 equipee, + uint8 equipSlot, + bytes24 equipment + ) internal { return state.set(Rel.Equip.selector, equipSlot, equipee, equipment, 1); } - function getEquipSlot(State state, bytes24 equipee, uint8 equipSlot) internal view returns (bytes24 equipedThing) { - (bytes24 thing,) = state.get(Rel.Equip.selector, equipSlot, equipee); + function getEquipSlot( + State state, + bytes24 equipee, + uint8 equipSlot + ) internal view returns (bytes24 equipedThing) { + (bytes24 thing, ) = state.get(Rel.Equip.selector, equipSlot, equipee); return thing; } - function setImplementation(State state, bytes24 customizableThing, address contractAddr) internal { - return state.set(Rel.Implementation.selector, 0x0, customizableThing, Node.Extension(contractAddr), 0); - } - - function getImplementation(State state, bytes24 customizableThing) internal view returns (address) { - (bytes24 contractNode,) = state.get(Rel.Implementation.selector, 0x0, customizableThing); + function setImplementation( + State state, + bytes24 customizableThing, + address contractAddr + ) internal { + return + state.set( + Rel.Implementation.selector, + 0x0, + customizableThing, + Node.Extension(contractAddr), + 0 + ); + } + + function getImplementation( + State state, + bytes24 customizableThing + ) internal view returns (address) { + (bytes24 contractNode, ) = state.get( + Rel.Implementation.selector, + 0x0, + customizableThing + ); return address(uint160(uint192(contractNode))); } - function setBuildingKind(State state, bytes24 buildingInstance, bytes24 buildingKind) internal { - return state.set(Rel.Is.selector, 0x0, buildingInstance, buildingKind, 0); + function setBuildingKind( + State state, + bytes24 buildingInstance, + bytes24 buildingKind + ) internal { + return + state.set(Rel.Is.selector, 0x0, buildingInstance, buildingKind, 0); } - function getBuildingKind(State state, bytes24 buildingInstance) internal view returns (bytes24) { - (bytes24 kind,) = state.get(Rel.Is.selector, 0x0, buildingInstance); + function getBuildingKind( + State state, + bytes24 buildingInstance + ) internal view returns (bytes24) { + (bytes24 kind, ) = state.get(Rel.Is.selector, 0x0, buildingInstance); return kind; } - function getBuildingKindInfo(State, /*state*/ bytes24 buildingKind) - internal - pure - returns (uint64 id, BuildingCategory category) - { - id = uint64(uint192(buildingKind) >> 64 & type(uint64).max); - category = BuildingCategory(uint64(uint192(buildingKind) & type(uint64).max)); + function getBuildingKindInfo( + State, + /*state*/ bytes24 buildingKind + ) internal pure returns (uint64 id, BuildingCategory category) { + id = uint64((uint192(buildingKind) >> 64) & type(uint64).max); + category = BuildingCategory( + uint64(uint192(buildingKind) & type(uint64).max) + ); } - function getItemStructure(State, /*state*/ bytes24 item) - internal - pure - returns (uint32[3] memory atoms, bool isStackable) - { + function getItemStructure( + State, + /*state*/ bytes24 item + ) internal pure returns (uint32[3] memory atoms, bool isStackable) { isStackable = uint32(uint192(item) >> 96) == 1; atoms[GOO_GREEN] = uint32(uint192(item) >> 64); atoms[GOO_BLUE] = uint32(uint192(item) >> 32); @@ -361,8 +581,11 @@ library Schema { return (atoms, isStackable); } - function getAtoms(State state, bytes24 item) internal pure returns (uint32[3] memory atoms) { - (atoms,) = getItemStructure(state, item); + function getAtoms( + State state, + bytes24 item + ) internal pure returns (uint32[3] memory atoms) { + (atoms, ) = getItemStructure(state, item); return atoms; } @@ -370,22 +593,37 @@ library Schema { state.set(Rel.Supports.selector, 0x0, plugin, target, 0); } - function getPlugin(State state, bytes24 node) internal view returns (bytes24) { - (bytes24 plugin,) = state.get(Rel.Supports.selector, 0x0, node); + function getPlugin( + State state, + bytes24 node + ) internal view returns (bytes24) { + (bytes24 plugin, ) = state.get(Rel.Supports.selector, 0x0, node); return plugin; } - function getHash(State state, bytes24 node, uint8 edgeIndex) internal view returns (bytes20 hash) { - (bytes24 hashNode,) = state.get(Rel.Has.selector, edgeIndex, node); + function getHash( + State state, + bytes24 node, + uint8 edgeIndex + ) internal view returns (bytes20 hash) { + (bytes24 hashNode, ) = state.get(Rel.Has.selector, edgeIndex, node); hash = bytes20(uint160(uint192(hashNode) & type(uint160).max)); } - function setHash(State state, bytes20 hash, bytes24 node, uint8 edgeIndex) internal { + function setHash( + State state, + bytes20 hash, + bytes24 node, + uint8 edgeIndex + ) internal { state.set(Rel.Has.selector, edgeIndex, node, Node.Hash(hash), 0); } - function getID(State state, bytes24 node) internal view returns (bytes24 id) { - (id,) = state.get(Rel.ID.selector, 0, node); + function getID( + State state, + bytes24 node + ) internal view returns (bytes24 id) { + (id, ) = state.get(Rel.ID.selector, 0, node); } function setID(State state, bytes24 node, bytes24 idNode) internal { @@ -393,132 +631,338 @@ library Schema { state.setOwner(idNode, node); } - function setInput(State state, bytes24 kind, uint8 slot, bytes24 item, uint64 qty) internal { + function setInput( + State state, + bytes24 kind, + uint8 slot, + bytes24 item, + uint64 qty + ) internal { return state.set(Rel.Input.selector, slot, kind, item, qty); } - function getInput(State state, bytes24 kind, uint8 slot) internal view returns (bytes24 item, uint64 qty) { + function getInput( + State state, + bytes24 kind, + uint8 slot + ) internal view returns (bytes24 item, uint64 qty) { return state.get(Rel.Input.selector, slot, kind); } - function setOutput(State state, bytes24 kind, uint8 slot, bytes24 item, uint64 qty) internal { + function setOutput( + State state, + bytes24 kind, + uint8 slot, + bytes24 item, + uint64 qty + ) internal { return state.set(Rel.Output.selector, slot, kind, item, qty); } - function getOutput(State state, bytes24 kind, uint8 slot) internal view returns (bytes24 item, uint64 qty) { + function getOutput( + State state, + bytes24 kind, + uint8 slot + ) internal view returns (bytes24 item, uint64 qty) { return state.get(Rel.Output.selector, slot, kind); } - function setMaterial(State state, bytes24 kind, uint8 slot, bytes24 item, uint64 qty) internal { + function setMaterial( + State state, + bytes24 kind, + uint8 slot, + bytes24 item, + uint64 qty + ) internal { return state.set(Rel.Material.selector, slot, kind, item, qty); } - function getMaterial(State state, bytes24 kind, uint8 slot) internal view returns (bytes24 item, uint64 qty) { + function getMaterial( + State state, + bytes24 kind, + uint8 slot + ) internal view returns (bytes24 item, uint64 qty) { return state.get(Rel.Material.selector, slot, kind); } - function getIsFinalised(State state, bytes24 sessionID) internal view returns (bool) { - ( /*bytes24 sessionNode*/ , uint64 isFinalised) = state.get(Rel.IsFinalised.selector, 0, sessionID); + function getIsFinalised( + State state, + bytes24 sessionID + ) internal view returns (bool) { + (, /*bytes24 sessionNode*/ uint64 isFinalised) = state.get( + Rel.IsFinalised.selector, + 0, + sessionID + ); return isFinalised > 0; } - function setIsFinalised(State state, bytes24 sessionID, bool isFinalised) internal { - state.set(Rel.IsFinalised.selector, 0, sessionID, sessionID, isFinalised ? 1 : 0); + function setIsFinalised( + State state, + bytes24 sessionID, + bool isFinalised + ) internal { + state.set( + Rel.IsFinalised.selector, + 0, + sessionID, + sessionID, + isFinalised ? 1 : 0 + ); } - function getSid(State, /*state*/ bytes24 mobileUnitID) internal pure returns (uint32) { + function getSid( + State, + /*state*/ bytes24 mobileUnitID + ) internal pure returns (uint32) { // NOTE: This is intentional. Where 'sid' is reauired by actions, it is typed as uint32 return uint32(CompoundKeyDecoder.UINT64(mobileUnitID)); } - function setTileAtomValues(State state, bytes24 tile, uint64[3] memory atoms) internal { - state.set(Rel.Balance.selector, GOO_GREEN, tile, Node.Atom(GOO_GREEN), atoms[GOO_GREEN]); - state.set(Rel.Balance.selector, GOO_BLUE, tile, Node.Atom(GOO_BLUE), atoms[GOO_BLUE]); - state.set(Rel.Balance.selector, GOO_RED, tile, Node.Atom(GOO_RED), atoms[GOO_RED]); + function setTileAtomValues( + State state, + bytes24 tile, + uint64[3] memory atoms + ) internal { + state.set( + Rel.Balance.selector, + GOO_GREEN, + tile, + Node.Atom(GOO_GREEN), + atoms[GOO_GREEN] + ); + state.set( + Rel.Balance.selector, + GOO_BLUE, + tile, + Node.Atom(GOO_BLUE), + atoms[GOO_BLUE] + ); + state.set( + Rel.Balance.selector, + GOO_RED, + tile, + Node.Atom(GOO_RED), + atoms[GOO_RED] + ); } - function getTileAtomValues(State state, bytes24 tile) internal view returns (uint64[3] memory atoms) { + function getTileAtomValues( + State state, + bytes24 tile + ) internal view returns (uint64[3] memory atoms) { uint64 atomVal; - ( /*bytes24*/ , atomVal) = state.get(Rel.Balance.selector, GOO_GREEN, tile); + (, /*bytes24*/ atomVal) = state.get( + Rel.Balance.selector, + GOO_GREEN, + tile + ); atoms[GOO_GREEN] = atomVal; - ( /*bytes24*/ , atomVal) = state.get(Rel.Balance.selector, GOO_BLUE, tile); + (, /*bytes24*/ atomVal) = state.get( + Rel.Balance.selector, + GOO_BLUE, + tile + ); atoms[GOO_BLUE] = atomVal; - ( /*bytes24*/ , atomVal) = state.get(Rel.Balance.selector, GOO_RED, tile); + (, /*bytes24*/ atomVal) = state.get( + Rel.Balance.selector, + GOO_RED, + tile + ); atoms[GOO_RED] = atomVal; } - function setBuildingReservoirAtoms(State state, bytes24 buildingInstance, uint64[3] memory atoms) internal { - state.set(Rel.Balance.selector, GOO_GREEN, buildingInstance, Node.Atom(GOO_GREEN), atoms[GOO_GREEN]); - state.set(Rel.Balance.selector, GOO_BLUE, buildingInstance, Node.Atom(GOO_BLUE), atoms[GOO_BLUE]); - state.set(Rel.Balance.selector, GOO_RED, buildingInstance, Node.Atom(GOO_RED), atoms[GOO_RED]); + function setBuildingReservoirAtoms( + State state, + bytes24 buildingInstance, + uint64[3] memory atoms + ) internal { + state.set( + Rel.Balance.selector, + GOO_GREEN, + buildingInstance, + Node.Atom(GOO_GREEN), + atoms[GOO_GREEN] + ); + state.set( + Rel.Balance.selector, + GOO_BLUE, + buildingInstance, + Node.Atom(GOO_BLUE), + atoms[GOO_BLUE] + ); + state.set( + Rel.Balance.selector, + GOO_RED, + buildingInstance, + Node.Atom(GOO_RED), + atoms[GOO_RED] + ); } - function getBuildingReservoirAtoms(State state, bytes24 buildingInstance) - internal - view - returns (uint64[3] memory atoms) - { + function getBuildingReservoirAtoms( + State state, + bytes24 buildingInstance + ) internal view returns (uint64[3] memory atoms) { uint64 atomVal; - ( /*bytes24*/ , atomVal) = state.get(Rel.Balance.selector, GOO_GREEN, buildingInstance); + (, /*bytes24*/ atomVal) = state.get( + Rel.Balance.selector, + GOO_GREEN, + buildingInstance + ); atoms[GOO_GREEN] = atomVal; - ( /*bytes24*/ , atomVal) = state.get(Rel.Balance.selector, GOO_BLUE, buildingInstance); + (, /*bytes24*/ atomVal) = state.get( + Rel.Balance.selector, + GOO_BLUE, + buildingInstance + ); atoms[GOO_BLUE] = atomVal; - ( /*bytes24*/ , atomVal) = state.get(Rel.Balance.selector, GOO_RED, buildingInstance); + (, /*bytes24*/ atomVal) = state.get( + Rel.Balance.selector, + GOO_RED, + buildingInstance + ); atoms[GOO_RED] = atomVal; } - function setBlockNum(State state, bytes24 kind, uint8 slot, uint64 blockNum) internal { - return state.set(Rel.HasBlockNum.selector, slot, kind, Node.BlockNum(), blockNum); - } - - function getBlockNum(State state, bytes24 kind, uint8 slot) internal view returns (uint64 blockNum) { - ( /*bytes24 item*/ , blockNum) = state.get(Rel.HasBlockNum.selector, slot, kind); - } - - function setBuildingConstructionBlockNum(State state, bytes24 buildingID, uint64 blockNum) internal { - state.setBlockNum(buildingID, uint8(BuildingBlockNumKey.CONSTRUCTION), blockNum); - } - - function getBuildingConstructionBlockNum(State state, bytes24 buildingID) internal view returns (uint64) { - return state.getBlockNum(buildingID, uint8(BuildingBlockNumKey.CONSTRUCTION)); + function setBlockNum( + State state, + bytes24 kind, + uint8 slot, + uint64 blockNum + ) internal { + return + state.set( + Rel.HasBlockNum.selector, + slot, + kind, + Node.BlockNum(), + blockNum + ); + } + + function getBlockNum( + State state, + bytes24 kind, + uint8 slot + ) internal view returns (uint64 blockNum) { + (, /*bytes24 item*/ blockNum) = state.get( + Rel.HasBlockNum.selector, + slot, + kind + ); } - function getTaskKind(State, /*state*/ bytes24 task) internal pure returns (uint32) { - return uint32(uint192(task) >> 32 & type(uint32).max); + function setBuildingConstructionBlockNum( + State state, + bytes24 buildingID, + uint64 blockNum + ) internal { + state.setBlockNum( + buildingID, + uint8(BuildingBlockNumKey.CONSTRUCTION), + blockNum + ); } - function setQuestAccepted(State state, bytes24 quest, bytes24 player, uint8 questNum) internal { - state.set(Rel.HasQuest.selector, questNum, player, quest, uint8(QuestStatus.ACCEPTED)); + function getBuildingConstructionBlockNum( + State state, + bytes24 buildingID + ) internal view returns (uint64) { + return + state.getBlockNum( + buildingID, + uint8(BuildingBlockNumKey.CONSTRUCTION) + ); + } + + function getTaskKind( + State, + /*state*/ bytes24 task + ) internal pure returns (uint32) { + return uint32((uint192(task) >> 32) & type(uint32).max); + } + + function setQuestAccepted( + State state, + bytes24 quest, + bytes24 player, + uint8 questNum + ) internal { + state.set( + Rel.HasQuest.selector, + questNum, + player, + quest, + uint8(QuestStatus.ACCEPTED) + ); } - function setQuestCompleted(State state, bytes24 quest, bytes24 player, uint8 questNum) internal { - state.set(Rel.HasQuest.selector, questNum, player, quest, uint8(QuestStatus.COMPLETED)); + function setQuestCompleted( + State state, + bytes24 quest, + bytes24 player, + uint8 questNum + ) internal { + state.set( + Rel.HasQuest.selector, + questNum, + player, + quest, + uint8(QuestStatus.COMPLETED) + ); } - function getPlayerQuest(State state, bytes24 player, uint8 questNum) internal view returns (bytes24, QuestStatus) { - (bytes24 quest, uint64 status) = state.get(Rel.HasQuest.selector, questNum, player); + function getPlayerQuest( + State state, + bytes24 player, + uint8 questNum + ) internal view returns (bytes24, QuestStatus) { + (bytes24 quest, uint64 status) = state.get( + Rel.HasQuest.selector, + questNum, + player + ); return (quest, QuestStatus(status)); } - function setData(State state, bytes24 nodeID, string memory key, bool data) internal { + function setData( + State state, + bytes24 nodeID, + string memory key, + bool data + ) internal { state.setData(nodeID, key, bytes32(uint256(data ? 1 : 0))); } - function setData(State state, bytes24 nodeID, string memory key, uint256 data) internal { + function setData( + State state, + bytes24 nodeID, + string memory key, + uint256 data + ) internal { state.setData(nodeID, key, bytes32(uint256(data))); } - function getDataBool(State state, bytes24 nodeID, string memory key) external view returns (bool) { + function getDataBool( + State state, + bytes24 nodeID, + string memory key + ) external view returns (bool) { return uint256(state.getData(nodeID, key)) == 1; } - function getDataUint256(State state, bytes24 nodeID, string memory key) external view returns (uint256) { + function getDataUint256( + State state, + bytes24 nodeID, + string memory key + ) external view returns (uint256) { return uint256(state.getData(nodeID, key)); } }