From 00891fef7cbbf6719388f51e4caf37b83114cbcb Mon Sep 17 00:00:00 2001 From: Billy Rennekamp Date: Thu, 18 Jan 2024 11:00:33 +0100 Subject: [PATCH 01/20] simple changes --- .../DuckBurger/DuckBurgerCounter.js | 233 +++--- contracts/src/example-plugins/MOBA/MOBA.js | 674 ++++++++++++++++++ contracts/src/example-plugins/MOBA/MOBA.sol | 620 ++++++++++++++++ contracts/src/example-plugins/MOBA/MOBA.yaml | 19 + .../src/example-plugins/MOBA/MOBACounter.js | 181 +++++ 5 files changed, 1610 insertions(+), 117 deletions(-) create mode 100644 contracts/src/example-plugins/MOBA/MOBA.js create mode 100644 contracts/src/example-plugins/MOBA/MOBA.sol create mode 100644 contracts/src/example-plugins/MOBA/MOBA.yaml create mode 100644 contracts/src/example-plugins/MOBA/MOBACounter.js 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/MOBA/MOBA.js b/contracts/src/example-plugins/MOBA/MOBA.js new file mode 100644 index 000000000..72f3a25aa --- /dev/null +++ b/contracts/src/example-plugins/MOBA/MOBA.js @@ -0,0 +1,674 @@ +import ds from "downstream"; + +const prizeFee = 2; +const prizeItemId = "0x6a7a67f063976de500000001000000010000000000000000"; // green goo +const buildingPrizeBagSlot = 0; +const buildingPrizeItemSlot = 0; +const nullBytes24 = `0x${"00".repeat(24)}`; +const duckBuildingTopId = "04"; +const burgerBuildingTopId = "17"; +const burgerCounterKindId = "Burger Display Building"; +const duckCounterKindId = "Duck Display Building"; +const countdownBuildingKindId = "Countdown Building"; + +let burgerCounter; +let duckCounter; +let countdownBuilding; +let startTime; +let endTime; + +export default async function update(state) { + + // + // Action handler functions + // + + // An action can set a form submit handler which will be called after the action along with the form values + let handleFormSubmit; + + const join = () => { + if (unitFeeBagSlot < 0) { + console.log( + "fee not found in bags - button should have been disabled", + ); + } + const mobileUnit = getMobileUnit(state); + + const payload = ds.encodeCall("function join()", []); + + const dummyBagIdIncaseToBagDoesNotExist = `0x${"00".repeat(24)}`; + + ds.dispatch( + { + name: "TRANSFER_ITEM_MOBILE_UNIT", + args: [ + mobileUnit.id, + [mobileUnit.id, selectedBuilding.id], + [unitFeeBagSlot, buildingPrizeBagSlot], + [unitFeeItemSlot, buildingPrizeItemSlot], + dummyBagIdIncaseToBagDoesNotExist, + prizeFee, + ], + }, + { + 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 selectedBuildingIdDuck = values["buildingKindIdDuck"]; + const selectedBuildingIdBurger = values["buildingKindIdBurger"]; + + console.log("start(): form.currentValues", values); + + // Verify selected buildings are different from each other + if (selectedBuildingIdDuck == selectedBuildingIdBurger) { + console.error("Team buildings must be different from each other", { + selectedBuildingIdDuck, + selectedBuildingIdBurger, + }); + return; + } + + const mobileUnit = getMobileUnit(state); + const payload = ds.encodeCall( + "function start(bytes24 duckBuildingID, bytes24 burgerBuildingID)", + [selectedBuildingIdDuck, selectedBuildingIdBurger], + ); + + ds.dispatch({ + name: "BUILDING_USE", + args: [selectedBuilding.id, mobileUnit.id, payload], + }); + }; + + const claim = () => { + const mobileUnit = getMobileUnit(state); + + const payload = ds.encodeCall("function claim()", []); + + 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 = "Duck Burger HQ"; + 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 DVB BUILDING FOUND"); + return { + version: 1, + map: [], + components: [ + { + id: "dbhq", + type: "building", + content: [ + { + id: "default", + type: "inline", + html: "", + buttons: [], + }, + ], + }, + ], + }; + } + + const { + prizePool, + gameActive, + startBlock, + endBlock, + buildingKindIdDuck, + buildingKindIdBurger, + teamDuckLength, + teamBurgerLength, + } = getHQData(selectedBuilding); + + const { unitFeeBagSlot, unitFeeItemSlot } = getMobileUnitFeeSlot(state); + const hasFee = unitFeeBagSlot >= 0; + const localBuildings = range5(state, selectedBuilding); + const duckCount = countBuildings( + localBuildings, + buildingKindIdDuck, + startBlock, + endBlock, + ); + const burgerCount = countBuildings( + localBuildings, + buildingKindIdBurger, + startBlock, + endBlock, + ); + + connectDisplayBuildings(state, localBuildings); + + + // 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 = "

Ducks vs Burgers HQ

"; + htmlBlock += `

payout for win: ${prizeFee * 2}

`; + htmlBlock += `

payout for draw: ${prizeFee}


`; + + + const canJoin = !gameActive && hasFee; + const canStart = !gameActive && teamDuckLength > 0 && teamBurgerLength > 0; + + if (canJoin) { + htmlBlock += `

total players: ${teamDuckLength + teamBurgerLength}


`; + } + + // Show what team the unit is on + const mobileUnit = getMobileUnit(state); + let isOnTeam = false; + if (mobileUnit){ + let unitTeam = ''; + + for (let i = 0; i < teamDuckLength; i++) { + if (mobileUnit.id == getHQTeamUnit(selectedBuilding, "Duck", i)) { + unitTeam = '🐤'; + break; + } + } + + if (unitTeam === '') { + for (let i = 0; i < teamBurgerLength; i++) { + if (mobileUnit.id == getHQTeamUnit(selectedBuilding, "Burger", i)) { + unitTeam = '🍔'; + break; + } + } + } + + if (unitTeam !== '') { + isOnTeam = true; + htmlBlock += ` +

You are on team ${unitTeam}


+ `; + } + } + + if (!gameActive){ + if (!isOnTeam){ + buttonList.push({ + text: `Join Game (${prizeFee} Green Goo)`, + type: "action", + action: join, + disabled: !canJoin || isOnTeam, + }); + }else{ + // Check reason why game can't start + const waitingForStartCondition = teamDuckLength != teamBurgerLength || teamDuckLength + teamBurgerLength < 2; + let startConditionMessage = ""; + if (waitingForStartCondition){ + if (teamDuckLength + teamBurgerLength < 2){ + startConditionMessage = "Waiting for players..." + } else if (teamDuckLength != teamBurgerLength){ + startConditionMessage = "Teams must be balanced..."; + } + } + + buttonList.push({ + text: waitingForStartCondition ? startConditionMessage : "Start", + type: "action", + action: start, + disabled: !canStart || teamDuckLength != teamBurgerLength, + }); + } + } + + if (canStart) { + // Show options to select team buildings + htmlBlock += ` +

Select Team Buildings

+

Team 🐤

+ ${getBuildingKindSelectHtml( + state, + duckBuildingTopId, + "buildingKindIdDuck", + )} +

Team 🍔

+ ${getBuildingKindSelectHtml( + state, + burgerBuildingTopId, + "buildingKindIdBurger", + )} + `; + } + + const nowBlock = state?.world?.block; + const blocksLeft = endBlock > nowBlock ? endBlock - nowBlock : 0; + const blocksFromStart = startBlock < nowBlock ? nowBlock - startBlock : 30; + const timeLeftMs = blocksLeft * 2 * 1000; + const timeSinceStartMs = blocksFromStart * 2 * 1000; + + if (gameActive) { + // Display selected team buildings + const buildingKindDuck = + state.world.buildingKinds.find( + (b) => b.id === buildingKindIdDuck, + ) || {}; + const buildingKindBurger = + state.world.buildingKinds.find( + (b) => b.id === buildingKindIdBurger, + ) || {}; + htmlBlock += ` +

Team Buildings:

+

Team 🐤: ${buildingKindDuck.name?.value}

+

Team 🍔: ${buildingKindBurger.name?.value}


+ + `; + + if (blocksLeft > 0) { + const now = Date.now(); + if (!startTime) startTime = now - timeSinceStartMs; + if (!endTime) endTime = now + timeLeftMs; + htmlBlock += `

time remaining: ${formatTime(timeLeftMs)}

`; + } else { + // End of game + buttonList.push({ + text: prizePool > 0 ? `Claim Reward` : "Nothing to Claim", + type: "action", + action: claim, + disabled: prizePool == 0, + }); + + htmlBlock += ` +

Game Over:

+

Final Score: 🐤${duckCount} : 🍔${burgerCount} + `; + if (duckCount == burgerCount) { + htmlBlock += ` +

The result was a draw

+ `; + } else { + const winningTeamName = + duckCount > burgerCount ? "duck" : "burger"; + const winningTeamEmoji = duckCount > burgerCount ? "🐤" : "🍔"; + htmlBlock += ` +

Team ${winningTeamName} have won the match!

+

${winningTeamEmoji}🏆

+ `; + } + } + } else { + startTime = undefined; + endTime = undefined; + } + + // Reset is always offered (requires some trust!) + buttonList.push({ + text: "Reset", + type: "action", + action: reset, + disabled: false, + }); + + // build up an array o fmap objects which are used to update display buildings + // always show the current team counts + const mapObj = [ + { + type: "building", + id: `${burgerCounter ? burgerCounter.id : ""}`, + key: "labelText", + value: `${burgerCount}`, + }, + { + type: "building", + id: `${duckCounter ? duckCounter.id : ""}`, + key: "labelText", + value: `${duckCount}`, + }, + ]; + + // if the game is running show the time + if (gameActive && blocksLeft > 0) { + mapObj.push( + { + type: "building", + id: `${countdownBuilding ? countdownBuilding.id : ""}`, + key: "countdown-start", + value: `${startTime}`, + }, + { + type: "building", + id: `${countdownBuilding ? countdownBuilding.id : ""}`, + key: "countdown-end", + value: `${endTime}`, + }, + ); + } else { + mapObj.push({ + type: "building", + id: `${countdownBuilding ? countdownBuilding.id : ""}`, + key: "labelText", + value: "", + }); + } + + return { + version: 1, + map: mapObj, + components: [ + { + id: "dbhq", + type: "building", + content: [ + { + id: "default", + type: "inline", + html: htmlBlock, + submit: (values) => { + if (typeof handleFormSubmit == "function") { + handleFormSubmit(values); + } + }, + buttons: buttonList, + }, + ], + }, + ], + }; +} + +// --- Duckbur HQ Specific functions + +function getHQData(selectedBuilding) { + const prizePool = getDataInt(selectedBuilding, "prizePool"); + const gameActive = getDataBool(selectedBuilding, "gameActive"); + const startBlock = getDataInt(selectedBuilding, "startBlock"); + const endBlock = getDataInt(selectedBuilding, "endBlock"); + const buildingKindIdDuck = getDataBytes24( + selectedBuilding, + "buildingKindIdDuck", + ); + const buildingKindIdBurger = getDataBytes24( + selectedBuilding, + "buildingKindIdBurger", + ); + const teamDuckLength = getDataInt(selectedBuilding, "teamDuckLength"); + const teamBurgerLength = getDataInt(selectedBuilding, "teamBurgerLength"); + + return { + prizePool, + gameActive, + startBlock, + endBlock, + startBlock, + buildingKindIdDuck, + buildingKindIdBurger, + teamDuckLength, + teamBurgerLength, + }; +} + +function getHQTeamUnit(selectedBuilding, team, index){ + return getDataBytes24(selectedBuilding, `team${team}Unit_${index}`); +} + +// search the buildings list ofr the display buildings we're gpoing to use +// for team counts and coutdown +function connectDisplayBuildings(state, buildings) { + if (!burgerCounter) { + burgerCounter = buildings.find((element) => + getBuildingKindsByTileLocation(state, element, burgerCounterKindId), + ); + } + if (!duckCounter) { + duckCounter = buildings.find((element) => + getBuildingKindsByTileLocation(state, element, duckCounterKindId), + ); + } + if (!countdownBuilding) { + countdownBuilding = buildings.find((element) => + getBuildingKindsByTileLocation( + state, + element, + countdownBuildingKindId, + ), + ); + } +} + +function formatTime(timeInMs) { + let seconds = Math.floor(timeInMs / 1000); + let minutes = Math.floor(seconds / 60); + let hours = Math.floor(minutes / 60); + + seconds %= 60; + minutes %= 60; + + // Pad each component to ensure two digits + let formattedHours = String(hours).padStart(2, "0"); + let formattedMinutes = String(minutes).padStart(2, "0"); + let formattedSeconds = String(seconds).padStart(2, "0"); + + return `${formattedHours}:${formattedMinutes}:${formattedSeconds}`; +} + +const countBuildings = (buildingsArray, kindID, startBlock, endBlock) => { + return buildingsArray.filter( + (b) => + b.kind?.id == kindID && + b.constructionBlockNum.value >= startBlock && + b.constructionBlockNum.value <= endBlock, + ).length; +}; + +function getMobileUnitFeeSlot(state) { + const mobileUnit = getMobileUnit(state); + const mobileUnitBags = mobileUnit ? getEquipeeBags(state, mobileUnit) : []; + const { bag, slotKey } = findBagAndSlot( + mobileUnitBags, + prizeItemId, + prizeFee, + ); + const unitFeeBagSlot = bag ? bag.equipee.key : -1; + const unitFeeItemSlot = bag ? slotKey : -1; + return { + unitFeeBagSlot, + unitFeeItemSlot, + }; +} + +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]), + ); +} + +function getBuildingKindsByTileLocation(state, building, kindID) { + return (state?.world?.buildings || []).find( + (b) => b.id === building.id && b.kind?.name?.value == kindID, + ); +} + +// get first slot in bags that matches item requirements +function findBagAndSlot(bags, requiredItemId, requiredBalance) { + for (const bag of bags) { + for (const slotKey in bag.slots) { + const slot = bag.slots[slotKey]; + if ( + (!requiredItemId || slot.item.id == requiredItemId) && + requiredBalance <= slot.balance + ) { + return { + bag: bag, + slotKey: slot.key, // assuming each slot has a 'key' property + }; + } + } + } + return { bag: null, slotKey: -1 }; +} + +// -- 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; + }, {}); +} + +// 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..9b26e1ef2 --- /dev/null +++ b/contracts/src/example-plugins/MOBA/MOBA.sol @@ -0,0 +1,620 @@ +// 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 { + // MOBA TODO: instead of counting the number of buildings, check whether BaseA and BaseB exist in the game + + // todo - storing contract members like this is per BuildingKind + // to work with building instances and therefore allow multiple buildings + // this data should be stored either as a map to buildingInstance + // or only use SET_DATA_ON_BUILDING action + bytes24[] private redTeam; + bytes24[] private bleTeam; + + // consts + // prize bag info + uint8 constant prizeBagSlot = 0; + uint8 constant prizeItemSlot = 0; + uint64 constant joinFee = 2; + + // 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 { + 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 + ); + } else if ((bytes4)(payload) == this.claim.selector) { + _claim(ds, state, actor, buildingInstance); + } else if ((bytes4)(payload) == this.reset.selector) { + _reset(ds, buildingInstance); + } + + ds.getDispatcher().dispatch( + abi.encodeCall( + Actions.SET_DATA_ON_BUILDING, + ( + buildingInstance, + "prizePool", + bytes32(uint256(_calculatePool())) + ) + ) + ); + } + + 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) { + revert("Can't join while a game is already active"); + } + + // TODO: remove prize stuff + + // // 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 currentPrizeBalance = _getPrizeBalance(state, buildingId); + // if ((currentPrizeBalance - joinFee) < lastKnownPrizeBalance) { + // revert("Fee not paid"); + // } + + // // remember the new balance + // Dispatcher dispatcher = ds.getDispatcher(); + // dispatcher.dispatch( + // abi.encodeCall( + // Actions.SET_DATA_ON_BUILDING, + // ( + // buildingId, + // "lastKnownPrizeBalance", + // bytes32(uint256(currentPrizeBalance)) + // ) + // ) + // ); + + 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); + } + } + + 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"); + } + + // 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)) + ) + ); + + // TODO: remove time stuff + + // // 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))) + // ) + // ); + + // // 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)) + // ) + // ) + // ); + + // // set start to now + // dispatcher.dispatch( + // 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))) + ) + ); + } + + function _claim( + Game ds, + State state, + bytes24 unitId, + bytes24 buildingId + ) private { + // check game finished + + // TODO: remove time stuff + // { + // uint256 endBlock = uint256(state.getData(buildingId, "endBlock")); + // if (block.number < endBlock) { + // revert("Can't claim, game is running"); + // } + // } + + // check unit in a team + // check unit not already claimed + bool isRedTeamMember = false; + bool isBlueTeamMember = false; + for (uint256 i = 0; i < redTeam.length; i++) { + if (redTeam[i] == unitId) { + isRedTeamMember = true; + break; + } + } + for (uint256 i = 0; i < blueTeam.length; i++) { + if (blueTeam[i] == unitId) { + isBlueTeamMember = true; + break; + } + } + require( + isRedTeamMember || isBlueTeamMember, + "Unit did not play or has already claimed" + ); + + // count buildings for each team + // NOTE: Scoped to avoid stack being too deep + bool isDraw; + { + (uint24 redBuildings, uint24 blueBuildings) = getBuildingCounts( + state, + buildingId + ); + + if (redBuildings > 1) { + revert("Can't be more than one Red Base"); + } + if (blueBuildings > 1) { + revert("Can't be more than one Blue Base"); + } + + if (redBuildings == 1 && blueBuildings == 1) { + revert("Game isn't over"); + } + + // check unit is in winning team + + if (isRedTeamMember && redBuildings < blueBuildings) { + revert("You, red, are not on the winning team: blue"); + } else if (isBlueTeamMember && blueBuildings < redBuildings) { + revert("You, blue, are not on the winning team: red"); + } + } + + // // winner! (or drawer) + // // \todo this currently assumes even teams + // Dispatcher dispatcher = ds.getDispatcher(); + // _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))) + // ) + // ) + // ); + + // Remove unit from team so they can't double claim + if (isRedTeamMember) { + removeUnitFromArray(redTeam, unitId); + } else if (isBlueTeamMember) { + removeUnitFromArray(blueteam, unitId); + } + } + + // 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 + // ); + + // (uint8 destBagSlot, uint8 destItemSlot) = _findValidItemSlot( + // state, + // unitId, + // prizeItemId, + // prizeAmount + // ); + + // dispatcher.dispatch( + // abi.encodeCall( + // Actions.TRANSFER_ITEM_MOBILE_UNIT, + // ( + // buildingId, + // [buildingId, unitId], + // [prizeBagSlot, destBagSlot], + // [prizeItemSlot, destItemSlot], + // bytes24(0), // To bag ID not required + // prizeAmount + // ) + // ) + // ); + // } + + // 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" + // ); + + // for (destItemSlot = 0; destItemSlot < 4; destItemSlot++) { + // (bytes24 destItemId, uint64 destBalance) = state.getItemSlot( + // destBagId, + // destItemSlot + // ); + // if ( + // (destItemId == bytes24(0) || destItemId == itemId) && + // destBalance + transferAmount <= 100 + // ) { + // // Found valid slot + // return (destBagSlot, destItemSlot); + // } + // } + // } + + // revert("No valid slot for prize claim found"); + // } + + 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 :) + + // set state to joining (gameActive ?) + // dispatcher.dispatch( + // 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))) + // ) + // ); + 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, "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, "buildingKindIdRed", bytes32(0)) + ) + ); + dispatcher.dispatch( + abi.encodeCall( + Actions.SET_DATA_ON_BUILDING, + (buildingId, "buildingKindIdBlue", bytes32(0)) + ) + ); + } + + // function _getPrizeBalance( + // State state, + // bytes24 buildingId + // ) internal view returns (uint64) { + // bytes24 prizeBag = state.getEquipSlot(buildingId, prizeBagSlot); + // (, uint64 balance) = state.getItemSlot(prizeBag, prizeItemSlot); + // return balance; + // } + + // function _calculatePrizeAmount() internal pure returns (uint64) { + // return joinFee * 2; + // } + + // function _calculatePool() internal view returns (uint64) { + // return uint64(teamDuckUnits.length + teamBurgerUnits.length) * joinFee; + // } + + 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") + ); + // uint256 endBlock = uint256(state.getData(buildingInstance, "endBlock")); + // 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] + ); + if (state.getBuildingKind(arenaBuildingID) == redBuildingKind) { + // uint64 constructionBlockNum = state + // .getBuildingConstructionBlockNum(arenaBuildingID); + // if ( + // constructionBlockNum >= startBlock && + // constructionBlockNum <= endBlock + // ) { + reds++; + // } + } else if ( + state.getBuildingKind(arenaBuildingID) == blueBuildingKind + ) { + // uint64 constructionBlockNum = state + // .getBuildingConstructionBlockNum(arenaBuildingID); + // if ( + // constructionBlockNum >= startBlock && + // constructionBlockNum <= endBlock + // ) { + 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 From 2252d774946f75bcfb2b9e434208d76058477370 Mon Sep 17 00:00:00 2001 From: Billy Rennekamp Date: Thu, 18 Jan 2024 11:03:22 +0100 Subject: [PATCH 02/20] renaming --- .../DuckBurger/DuckBurgerHQ.sol | 625 ++++++++++++------ .../DuckBurger/DuckBurgerHQ.yaml | 11 +- .../DuckBurgerCounter_.js} | 0 .../MOBA.js => DuckBurger_/DuckBurgerHQ_.js} | 0 .../DuckBurger_/DuckBurgerHQ_.sol | 407 ++++++++++++ .../DuckBurgerHQ_.yaml} | 11 +- contracts/src/example-plugins/MOBA/MOBA.sol | 620 ----------------- 7 files changed, 837 insertions(+), 837 deletions(-) rename contracts/src/example-plugins/{MOBA/MOBACounter.js => DuckBurger_/DuckBurgerCounter_.js} (100%) rename contracts/src/example-plugins/{MOBA/MOBA.js => DuckBurger_/DuckBurgerHQ_.js} (100%) create mode 100644 contracts/src/example-plugins/DuckBurger_/DuckBurgerHQ_.sol rename contracts/src/example-plugins/{MOBA/MOBA.yaml => DuckBurger_/DuckBurgerHQ_.yaml} (62%) delete mode 100644 contracts/src/example-plugins/MOBA/MOBA.sol diff --git a/contracts/src/example-plugins/DuckBurger/DuckBurgerHQ.sol b/contracts/src/example-plugins/DuckBurger/DuckBurgerHQ.sol index 75b20c850..9b26e1ef2 100644 --- a/contracts/src/example-plugins/DuckBurger/DuckBurgerHQ.sol +++ b/contracts/src/example-plugins/DuckBurger/DuckBurgerHQ.sol @@ -11,13 +11,15 @@ import "@ds/utils/LibString.sol"; using Schema for State; -contract DuckBurgerHQ is BuildingKind { +contract MOBA is BuildingKind { + // MOBA TODO: instead of counting the number of buildings, check whether BaseA and BaseB exist in the game + // todo - storing contract members like this is per BuildingKind // to work with building instances and therefore allow multiple buildings // this data should be stored either as a map to buildingInstance // or only use SET_DATA_ON_BUILDING action - bytes24[] private teamDuckUnits; - bytes24[] private teamBurgerUnits; + bytes24[] private redTeam; + bytes24[] private bleTeam; // consts // prize bag info @@ -28,20 +30,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,60 +68,89 @@ 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) { revert("Can't join while a game is already active"); } - // 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 currentPrizeBalance = _getPrizeBalance(state, buildingId); - if ((currentPrizeBalance - joinFee) < lastKnownPrizeBalance) { - revert("Fee not paid"); + // TODO: remove prize stuff + + // // 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 currentPrizeBalance = _getPrizeBalance(state, buildingId); + // if ((currentPrizeBalance - joinFee) < lastKnownPrizeBalance) { + // revert("Fee not paid"); + // } + + // // remember the new balance + // Dispatcher dispatcher = ds.getDispatcher(); + // dispatcher.dispatch( + // abi.encodeCall( + // Actions.SET_DATA_ON_BUILDING, + // ( + // buildingId, + // "lastKnownPrizeBalance", + // bytes32(uint256(currentPrizeBalance)) + // ) + // ) + // ); + + for (uint256 i = 0; i < redTeam.length; i++) { + if (redTeam[i] == unitId) revert("Already joined"); } - - // remember the new balance - Dispatcher dispatcher = ds.getDispatcher(); - dispatcher.dispatch( - abi.encodeCall( - Actions.SET_DATA_ON_BUILDING, - (buildingId, "lastKnownPrizeBalance", bytes32(uint256(currentPrizeBalance))) - ) - ); - - for (uint256 i = 0; i < teamDuckUnits.length; i++) { - if (teamDuckUnits[i] == unitId) revert("Already joined"); - } - for (uint256 i = 0; i < teamBurgerUnits.length; i++) { - if (teamBurgerUnits[i] == unitId) revert("Already joined"); + for (uint256 i = 0; i < blueTeam.length; i++) { + if (blueTeam[i] == unitId) revert("Already joined"); } // Assign a team - if (teamDuckUnits.length <= teamBurgerUnits.length) { - teamDuckUnits.push(unitId); - assignUnitToTeam(ds, "duck", unitId, buildingId); + if (redTeam.length <= blueTeam.length) { + redTeam.push(unitId); + assignUnitToTeam(ds, "red", unitId, buildingId); } else { - teamBurgerUnits.push(unitId); - assignUnitToTeam(ds, "burger", unitId, buildingId); + blueTeam.push(unitId); + assignUnitToTeam(ds, "blue", unitId, buildingId); } } - 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("red")) + ) { + processTeam(dispatcher, buildingId, "redTeam", redTeam, unitId); + } else if ( + keccak256(abi.encodePacked(team)) == + keccak256(abi.encodePacked("blue")) + ) { + processTeam(dispatcher, buildingId, "blueTeam", blueTeam, unitId); } } @@ -117,172 +164,272 @@ 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 redBaseID, + bytes24 blueBaseID + ) 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")); - if (teamDuckLength == 0 || teamBurgerLength == 0) { + 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"); } // set team buildings - dispatcher.dispatch( - 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, "buildingKindIdRed", bytes32(redBaseID)) ) ); - - // 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)))) - ); - - // 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, "buildingKindIdBlue", bytes32(blueBaseID)) ) ); - // set start to now - dispatcher.dispatch( - abi.encodeCall(Actions.SET_DATA_ON_BUILDING, (buildingId, "startBlock", bytes32(uint256(block.number)))) - ); + // TODO: remove time stuff + + // // 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))) + // ) + // ); + + // // 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)) + // ) + // ) + // ); + + // // set start to now + // dispatcher.dispatch( + // 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")); - if (block.number < endBlock) { - revert("Can't claim, game is running"); - } - } + + // TODO: remove time stuff + // { + // uint256 endBlock = uint256(state.getData(buildingId, "endBlock")); + // if (block.number < endBlock) { + // revert("Can't claim, game is running"); + // } + // } // check unit in a team // check unit not already claimed - bool isDuckTeamMember = false; - bool isBurgerTeamMember = false; - for (uint256 i = 0; i < teamDuckUnits.length; i++) { - if (teamDuckUnits[i] == unitId) { - isDuckTeamMember = true; + bool isRedTeamMember = false; + bool isBlueTeamMember = false; + for (uint256 i = 0; i < redTeam.length; i++) { + if (redTeam[i] == unitId) { + isRedTeamMember = true; break; } } - for (uint256 i = 0; i < teamBurgerUnits.length; i++) { - if (teamBurgerUnits[i] == unitId) { - isBurgerTeamMember = true; + for (uint256 i = 0; i < blueTeam.length; i++) { + if (blueTeam[i] == unitId) { + isBlueTeamMember = true; break; } } - require(isDuckTeamMember || isBurgerTeamMember, "Unit did not play or has already claimed"); + require( + isRedTeamMember || isBlueTeamMember, + "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); - - // check unit is in winning team + (uint24 redBuildings, uint24 blueBuildings) = getBuildingCounts( + state, + buildingId + ); - if (isDuckTeamMember && duckBuildings < burgerBuildings) { - revert("You, duck, are not on the winning team: burgers"); - } else if (isBurgerTeamMember && burgerBuildings < duckBuildings) { - revert("You, burger, are not on the winning team: ducks"); + if (redBuildings > 1) { + revert("Can't be more than one Red Base"); + } + if (blueBuildings > 1) { + revert("Can't be more than one Blue Base"); } - isDraw = burgerBuildings == duckBuildings; - } - // winner! (or drawer) - // \todo this currently assumes even teams - Dispatcher dispatcher = ds.getDispatcher(); - _awardPrize(state, dispatcher, buildingId, unitId, isDraw ? joinFee : _calculatePrizeAmount()); + if (redBuildings == 1 && blueBuildings == 1) { + revert("Game isn't over"); + } - // remember new prize balance - dispatcher.dispatch( - abi.encodeCall( - Actions.SET_DATA_ON_BUILDING, - (buildingId, "lastKnownPrizeBalance", bytes32(uint256(_getPrizeBalance(state, buildingId)))) - ) - ); + // check unit is in winning team - // Remove unit from team so they can't double claim - if (isDuckTeamMember) { - removeUnitFromArray(teamDuckUnits, unitId); - } else if (isBurgerTeamMember) { - removeUnitFromArray(teamBurgerUnits, unitId); + if (isRedTeamMember && redBuildings < blueBuildings) { + revert("You, red, are not on the winning team: blue"); + } else if (isBlueTeamMember && blueBuildings < redBuildings) { + revert("You, blue, are not on the winning team: red"); + } } - } - 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); + // // winner! (or drawer) + // // \todo this currently assumes even teams + // Dispatcher dispatcher = ds.getDispatcher(); + // _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))) + // ) + // ) + // ); - (uint8 destBagSlot, uint8 destItemSlot) = _findValidItemSlot(state, unitId, prizeItemId, prizeAmount); - - dispatcher.dispatch( - abi.encodeCall( - Actions.TRANSFER_ITEM_MOBILE_UNIT, - ( - buildingId, - [buildingId, unitId], - [prizeBagSlot, destBagSlot], - [prizeItemSlot, destItemSlot], - bytes24(0), // To bag ID not required - prizeAmount - ) - ) - ); - } - - 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"); - - for (destItemSlot = 0; destItemSlot < 4; destItemSlot++) { - (bytes24 destItemId, uint64 destBalance) = state.getItemSlot(destBagId, destItemSlot); - if ((destItemId == bytes24(0) || destItemId == itemId) && destBalance + transferAmount <= 100) { - // Found valid slot - return (destBagSlot, destItemSlot); - } - } + // Remove unit from team so they can't double claim + if (isRedTeamMember) { + removeUnitFromArray(redTeam, unitId); + } else if (isBlueTeamMember) { + removeUnitFromArray(blueteam, unitId); } - - revert("No valid slot for prize claim found"); } - function removeUnitFromArray(bytes24[] storage array, bytes24 unitId) 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 + // ); + + // (uint8 destBagSlot, uint8 destItemSlot) = _findValidItemSlot( + // state, + // unitId, + // prizeItemId, + // prizeAmount + // ); + + // dispatcher.dispatch( + // abi.encodeCall( + // Actions.TRANSFER_ITEM_MOBILE_UNIT, + // ( + // buildingId, + // [buildingId, unitId], + // [prizeBagSlot, destBagSlot], + // [prizeItemSlot, destItemSlot], + // bytes24(0), // To bag ID not required + // prizeAmount + // ) + // ) + // ); + // } + + // 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" + // ); + + // for (destItemSlot = 0; destItemSlot < 4; destItemSlot++) { + // (bytes24 destItemId, uint64 destBalance) = state.getItemSlot( + // destBagId, + // destItemSlot + // ); + // if ( + // (destItemId == bytes24(0) || destItemId == itemId) && + // destBalance + transferAmount <= 100 + // ) { + // // Found valid slot + // return (destBagSlot, destItemSlot); + // } + // } + // } + + // revert("No valid slot for prize claim found"); + // } + + 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]; @@ -299,71 +446,125 @@ contract DuckBurgerHQ is BuildingKind { // for now allwing reset any time which requires some trust :) // set state to joining (gameActive ?) + // dispatcher.dispatch( + // 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))) + // ) + // ); dispatcher.dispatch( - 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, "gameActive", bytes32(uint256(0))) + ) ); + delete redTeam; + delete blueteam; dispatcher.dispatch( - abi.encodeCall(Actions.SET_DATA_ON_BUILDING, (buildingId, "gameActive", bytes32(uint256(0)))) + abi.encodeCall( + Actions.SET_DATA_ON_BUILDING, + (buildingId, "blueTeamLength", bytes32(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, "redTeamLength", 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, "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))) + abi.encodeCall( + Actions.SET_DATA_ON_BUILDING, + (buildingId, "buildingKindIdRed", bytes32(0)) + ) ); dispatcher.dispatch( - abi.encodeCall(Actions.SET_DATA_ON_BUILDING, (buildingId, "buildingKindIdBurger", bytes32(0))) + abi.encodeCall( + Actions.SET_DATA_ON_BUILDING, + (buildingId, "buildingKindIdBlue", bytes32(0)) + ) ); } - function _getPrizeBalance(State state, bytes24 buildingId) internal view returns (uint64) { - bytes24 prizeBag = state.getEquipSlot(buildingId, prizeBagSlot); - (, uint64 balance) = state.getItemSlot(prizeBag, prizeItemSlot); - return balance; - } - - function _calculatePrizeAmount() internal pure returns (uint64) { - return joinFee * 2; - } - - function _calculatePool() internal view returns (uint64) { - 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")); - uint256 endBlock = uint256(state.getData(buildingInstance, "endBlock")); - uint256 startBlock = uint256(state.getData(buildingInstance, "startBlock")); + // function _getPrizeBalance( + // State state, + // bytes24 buildingId + // ) internal view returns (uint64) { + // bytes24 prizeBag = state.getEquipSlot(buildingId, prizeBagSlot); + // (, uint64 balance) = state.getItemSlot(prizeBag, prizeItemSlot); + // return balance; + // } + + // function _calculatePrizeAmount() internal pure returns (uint64) { + // return joinFee * 2; + // } + + // function _calculatePool() internal view returns (uint64) { + // return uint64(teamDuckUnits.length + teamBurgerUnits.length) * joinFee; + // } + + 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") + ); + // uint256 endBlock = uint256(state.getData(buildingInstance, "endBlock")); + // 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) { - ducks++; - } - } else if (state.getBuildingKind(arenaBuildingID) == burgerBuildingKind) { - uint64 constructionBlockNum = state.getBuildingConstructionBlockNum(arenaBuildingID); - if (constructionBlockNum >= startBlock && constructionBlockNum <= endBlock) { - burgers++; - } + if (state.getBuildingKind(arenaBuildingID) == redBuildingKind) { + // uint64 constructionBlockNum = state + // .getBuildingConstructionBlockNum(arenaBuildingID); + // if ( + // constructionBlockNum >= startBlock && + // constructionBlockNum <= endBlock + // ) { + reds++; + // } + } else if ( + state.getBuildingKind(arenaBuildingID) == blueBuildingKind + ) { + // uint64 constructionBlockNum = state + // .getBuildingConstructionBlockNum(arenaBuildingID); + // if ( + // constructionBlockNum >= startBlock && + // constructionBlockNum <= endBlock + // ) { + blues++; + // } } } } @@ -372,12 +573,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 +596,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/DuckBurger/DuckBurgerHQ.yaml b/contracts/src/example-plugins/DuckBurger/DuckBurgerHQ.yaml index 5eb304abd..3a1266264 100644 --- a/contracts/src/example-plugins/DuckBurger/DuckBurgerHQ.yaml +++ b/contracts/src/example-plugins/DuckBurger/DuckBurgerHQ.yaml @@ -1,15 +1,14 @@ - kind: BuildingKind spec: - name: Duck Burger HQ - description: "Play Ducks vs Burgers" + name: MOBA + description: "Play MOBA" category: custom model: 11-03 color: 1 contract: - file: ./DuckBurgerHQ.sol + file: ./MOBA.sol plugin: - file: ./DuckBurgerHQ.js + file: ./MOBA.js alwaysActive: true materials: - name: Green Goo @@ -17,4 +16,4 @@ spec: - name: Blue Goo quantity: 10 - name: Red Goo - quantity: 10 \ No newline at end of file + quantity: 10 diff --git a/contracts/src/example-plugins/MOBA/MOBACounter.js b/contracts/src/example-plugins/DuckBurger_/DuckBurgerCounter_.js similarity index 100% rename from contracts/src/example-plugins/MOBA/MOBACounter.js rename to contracts/src/example-plugins/DuckBurger_/DuckBurgerCounter_.js diff --git a/contracts/src/example-plugins/MOBA/MOBA.js b/contracts/src/example-plugins/DuckBurger_/DuckBurgerHQ_.js similarity index 100% rename from contracts/src/example-plugins/MOBA/MOBA.js rename to contracts/src/example-plugins/DuckBurger_/DuckBurgerHQ_.js diff --git a/contracts/src/example-plugins/DuckBurger_/DuckBurgerHQ_.sol b/contracts/src/example-plugins/DuckBurger_/DuckBurgerHQ_.sol new file mode 100644 index 000000000..75b20c850 --- /dev/null +++ b/contracts/src/example-plugins/DuckBurger_/DuckBurgerHQ_.sol @@ -0,0 +1,407 @@ +// 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 DuckBurgerHQ is BuildingKind { + // todo - storing contract members like this is per BuildingKind + // to work with building instances and therefore allow multiple buildings + // this data should be stored either as a map to buildingInstance + // or only use SET_DATA_ON_BUILDING action + bytes24[] private teamDuckUnits; + bytes24[] private teamBurgerUnits; + + // consts + // prize bag info + uint8 constant prizeBagSlot = 0; + uint8 constant prizeItemSlot = 0; + uint64 constant joinFee = 2; + + // 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 { + 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); + } else if ((bytes4)(payload) == this.claim.selector) { + _claim(ds, state, actor, buildingInstance); + } else if ((bytes4)(payload) == this.reset.selector) { + _reset(ds, buildingInstance); + } + + ds.getDispatcher().dispatch( + abi.encodeCall( + Actions.SET_DATA_ON_BUILDING, (buildingInstance, "prizePool", bytes32(uint256(_calculatePool()))) + ) + ); + } + + 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) { + revert("Can't join while a game is already active"); + } + + // 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 currentPrizeBalance = _getPrizeBalance(state, buildingId); + if ((currentPrizeBalance - joinFee) < lastKnownPrizeBalance) { + revert("Fee not paid"); + } + + // remember the new balance + Dispatcher dispatcher = ds.getDispatcher(); + dispatcher.dispatch( + abi.encodeCall( + Actions.SET_DATA_ON_BUILDING, + (buildingId, "lastKnownPrizeBalance", bytes32(uint256(currentPrizeBalance))) + ) + ); + + for (uint256 i = 0; i < teamDuckUnits.length; i++) { + if (teamDuckUnits[i] == unitId) revert("Already joined"); + } + for (uint256 i = 0; i < teamBurgerUnits.length; i++) { + if (teamBurgerUnits[i] == unitId) revert("Already joined"); + } + + // Assign a team + if (teamDuckUnits.length <= teamBurgerUnits.length) { + teamDuckUnits.push(unitId); + assignUnitToTeam(ds, "duck", unitId, buildingId); + } else { + teamBurgerUnits.push(unitId); + assignUnitToTeam(ds, "burger", 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("duck"))) { + processTeam(dispatcher, buildingId, "teamDuck", teamDuckUnits, unitId); + } else if (keccak256(abi.encodePacked(team)) == keccak256(abi.encodePacked("burger"))) { + processTeam(dispatcher, buildingId, "teamBurger", teamBurgerUnits, unitId); + } + } + + 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 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")); + 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))) + ); + dispatcher.dispatch( + abi.encodeCall( + 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)))) + ); + + // 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))) + ) + ); + + // set start to now + dispatcher.dispatch( + 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)))) + ); + } + + function _claim(Game ds, State state, bytes24 unitId, bytes24 buildingId) private { + // check game finished + { + uint256 endBlock = uint256(state.getData(buildingId, "endBlock")); + if (block.number < endBlock) { + revert("Can't claim, game is running"); + } + } + + // check unit in a team + // check unit not already claimed + bool isDuckTeamMember = false; + bool isBurgerTeamMember = false; + for (uint256 i = 0; i < teamDuckUnits.length; i++) { + if (teamDuckUnits[i] == unitId) { + isDuckTeamMember = true; + break; + } + } + for (uint256 i = 0; i < teamBurgerUnits.length; i++) { + if (teamBurgerUnits[i] == unitId) { + isBurgerTeamMember = true; + break; + } + } + 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); + + // check unit is in winning team + + if (isDuckTeamMember && duckBuildings < burgerBuildings) { + revert("You, duck, are not on the winning team: burgers"); + } else if (isBurgerTeamMember && burgerBuildings < duckBuildings) { + revert("You, burger, are not on the winning team: ducks"); + } + isDraw = burgerBuildings == duckBuildings; + } + + // winner! (or drawer) + // \todo this currently assumes even teams + Dispatcher dispatcher = ds.getDispatcher(); + _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)))) + ) + ); + + // Remove unit from team so they can't double claim + if (isDuckTeamMember) { + removeUnitFromArray(teamDuckUnits, unitId); + } else if (isBurgerTeamMember) { + removeUnitFromArray(teamBurgerUnits, unitId); + } + } + + 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); + + (uint8 destBagSlot, uint8 destItemSlot) = _findValidItemSlot(state, unitId, prizeItemId, prizeAmount); + + dispatcher.dispatch( + abi.encodeCall( + Actions.TRANSFER_ITEM_MOBILE_UNIT, + ( + buildingId, + [buildingId, unitId], + [prizeBagSlot, destBagSlot], + [prizeItemSlot, destItemSlot], + bytes24(0), // To bag ID not required + prizeAmount + ) + ) + ); + } + + 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"); + + for (destItemSlot = 0; destItemSlot < 4; destItemSlot++) { + (bytes24 destItemId, uint64 destBalance) = state.getItemSlot(destBagId, destItemSlot); + if ((destItemId == bytes24(0) || destItemId == itemId) && destBalance + transferAmount <= 100) { + // Found valid slot + return (destBagSlot, destItemSlot); + } + } + } + + revert("No valid slot for prize claim found"); + } + + 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 :) + + // set state to joining (gameActive ?) + dispatcher.dispatch( + 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)))) + ); + dispatcher.dispatch( + 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))) + ); + 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) { + bytes24 prizeBag = state.getEquipSlot(buildingId, prizeBagSlot); + (, uint64 balance) = state.getItemSlot(prizeBag, prizeItemSlot); + return balance; + } + + function _calculatePrizeAmount() internal pure returns (uint64) { + return joinFee * 2; + } + + function _calculatePool() internal view returns (uint64) { + 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")); + uint256 endBlock = uint256(state.getData(buildingInstance, "endBlock")); + 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] + ); + if (state.getBuildingKind(arenaBuildingID) == duckBuildingKind) { + 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) { + burgers++; + } + } + } + } + + 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/DuckBurger_/DuckBurgerHQ_.yaml similarity index 62% rename from contracts/src/example-plugins/MOBA/MOBA.yaml rename to contracts/src/example-plugins/DuckBurger_/DuckBurgerHQ_.yaml index 3a1266264..5eb304abd 100644 --- a/contracts/src/example-plugins/MOBA/MOBA.yaml +++ b/contracts/src/example-plugins/DuckBurger_/DuckBurgerHQ_.yaml @@ -1,14 +1,15 @@ + kind: BuildingKind spec: - name: MOBA - description: "Play MOBA" + name: Duck Burger HQ + description: "Play Ducks vs Burgers" category: custom model: 11-03 color: 1 contract: - file: ./MOBA.sol + file: ./DuckBurgerHQ.sol plugin: - file: ./MOBA.js + file: ./DuckBurgerHQ.js alwaysActive: true materials: - name: Green Goo @@ -16,4 +17,4 @@ spec: - name: Blue Goo quantity: 10 - name: Red Goo - quantity: 10 + quantity: 10 \ No newline at end of file diff --git a/contracts/src/example-plugins/MOBA/MOBA.sol b/contracts/src/example-plugins/MOBA/MOBA.sol deleted file mode 100644 index 9b26e1ef2..000000000 --- a/contracts/src/example-plugins/MOBA/MOBA.sol +++ /dev/null @@ -1,620 +0,0 @@ -// 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 { - // MOBA TODO: instead of counting the number of buildings, check whether BaseA and BaseB exist in the game - - // todo - storing contract members like this is per BuildingKind - // to work with building instances and therefore allow multiple buildings - // this data should be stored either as a map to buildingInstance - // or only use SET_DATA_ON_BUILDING action - bytes24[] private redTeam; - bytes24[] private bleTeam; - - // consts - // prize bag info - uint8 constant prizeBagSlot = 0; - uint8 constant prizeItemSlot = 0; - uint64 constant joinFee = 2; - - // 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 { - 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 - ); - } else if ((bytes4)(payload) == this.claim.selector) { - _claim(ds, state, actor, buildingInstance); - } else if ((bytes4)(payload) == this.reset.selector) { - _reset(ds, buildingInstance); - } - - ds.getDispatcher().dispatch( - abi.encodeCall( - Actions.SET_DATA_ON_BUILDING, - ( - buildingInstance, - "prizePool", - bytes32(uint256(_calculatePool())) - ) - ) - ); - } - - 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) { - revert("Can't join while a game is already active"); - } - - // TODO: remove prize stuff - - // // 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 currentPrizeBalance = _getPrizeBalance(state, buildingId); - // if ((currentPrizeBalance - joinFee) < lastKnownPrizeBalance) { - // revert("Fee not paid"); - // } - - // // remember the new balance - // Dispatcher dispatcher = ds.getDispatcher(); - // dispatcher.dispatch( - // abi.encodeCall( - // Actions.SET_DATA_ON_BUILDING, - // ( - // buildingId, - // "lastKnownPrizeBalance", - // bytes32(uint256(currentPrizeBalance)) - // ) - // ) - // ); - - 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); - } - } - - 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"); - } - - // 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)) - ) - ); - - // TODO: remove time stuff - - // // 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))) - // ) - // ); - - // // 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)) - // ) - // ) - // ); - - // // set start to now - // dispatcher.dispatch( - // 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))) - ) - ); - } - - function _claim( - Game ds, - State state, - bytes24 unitId, - bytes24 buildingId - ) private { - // check game finished - - // TODO: remove time stuff - // { - // uint256 endBlock = uint256(state.getData(buildingId, "endBlock")); - // if (block.number < endBlock) { - // revert("Can't claim, game is running"); - // } - // } - - // check unit in a team - // check unit not already claimed - bool isRedTeamMember = false; - bool isBlueTeamMember = false; - for (uint256 i = 0; i < redTeam.length; i++) { - if (redTeam[i] == unitId) { - isRedTeamMember = true; - break; - } - } - for (uint256 i = 0; i < blueTeam.length; i++) { - if (blueTeam[i] == unitId) { - isBlueTeamMember = true; - break; - } - } - require( - isRedTeamMember || isBlueTeamMember, - "Unit did not play or has already claimed" - ); - - // count buildings for each team - // NOTE: Scoped to avoid stack being too deep - bool isDraw; - { - (uint24 redBuildings, uint24 blueBuildings) = getBuildingCounts( - state, - buildingId - ); - - if (redBuildings > 1) { - revert("Can't be more than one Red Base"); - } - if (blueBuildings > 1) { - revert("Can't be more than one Blue Base"); - } - - if (redBuildings == 1 && blueBuildings == 1) { - revert("Game isn't over"); - } - - // check unit is in winning team - - if (isRedTeamMember && redBuildings < blueBuildings) { - revert("You, red, are not on the winning team: blue"); - } else if (isBlueTeamMember && blueBuildings < redBuildings) { - revert("You, blue, are not on the winning team: red"); - } - } - - // // winner! (or drawer) - // // \todo this currently assumes even teams - // Dispatcher dispatcher = ds.getDispatcher(); - // _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))) - // ) - // ) - // ); - - // Remove unit from team so they can't double claim - if (isRedTeamMember) { - removeUnitFromArray(redTeam, unitId); - } else if (isBlueTeamMember) { - removeUnitFromArray(blueteam, unitId); - } - } - - // 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 - // ); - - // (uint8 destBagSlot, uint8 destItemSlot) = _findValidItemSlot( - // state, - // unitId, - // prizeItemId, - // prizeAmount - // ); - - // dispatcher.dispatch( - // abi.encodeCall( - // Actions.TRANSFER_ITEM_MOBILE_UNIT, - // ( - // buildingId, - // [buildingId, unitId], - // [prizeBagSlot, destBagSlot], - // [prizeItemSlot, destItemSlot], - // bytes24(0), // To bag ID not required - // prizeAmount - // ) - // ) - // ); - // } - - // 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" - // ); - - // for (destItemSlot = 0; destItemSlot < 4; destItemSlot++) { - // (bytes24 destItemId, uint64 destBalance) = state.getItemSlot( - // destBagId, - // destItemSlot - // ); - // if ( - // (destItemId == bytes24(0) || destItemId == itemId) && - // destBalance + transferAmount <= 100 - // ) { - // // Found valid slot - // return (destBagSlot, destItemSlot); - // } - // } - // } - - // revert("No valid slot for prize claim found"); - // } - - 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 :) - - // set state to joining (gameActive ?) - // dispatcher.dispatch( - // 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))) - // ) - // ); - 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, "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, "buildingKindIdRed", bytes32(0)) - ) - ); - dispatcher.dispatch( - abi.encodeCall( - Actions.SET_DATA_ON_BUILDING, - (buildingId, "buildingKindIdBlue", bytes32(0)) - ) - ); - } - - // function _getPrizeBalance( - // State state, - // bytes24 buildingId - // ) internal view returns (uint64) { - // bytes24 prizeBag = state.getEquipSlot(buildingId, prizeBagSlot); - // (, uint64 balance) = state.getItemSlot(prizeBag, prizeItemSlot); - // return balance; - // } - - // function _calculatePrizeAmount() internal pure returns (uint64) { - // return joinFee * 2; - // } - - // function _calculatePool() internal view returns (uint64) { - // return uint64(teamDuckUnits.length + teamBurgerUnits.length) * joinFee; - // } - - 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") - ); - // uint256 endBlock = uint256(state.getData(buildingInstance, "endBlock")); - // 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] - ); - if (state.getBuildingKind(arenaBuildingID) == redBuildingKind) { - // uint64 constructionBlockNum = state - // .getBuildingConstructionBlockNum(arenaBuildingID); - // if ( - // constructionBlockNum >= startBlock && - // constructionBlockNum <= endBlock - // ) { - reds++; - // } - } else if ( - state.getBuildingKind(arenaBuildingID) == blueBuildingKind - ) { - // uint64 constructionBlockNum = state - // .getBuildingConstructionBlockNum(arenaBuildingID); - // if ( - // constructionBlockNum >= startBlock && - // constructionBlockNum <= endBlock - // ) { - 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(); - } -} From 064ed5a9df807042c1fa80fa66e2ac16879c599f Mon Sep 17 00:00:00 2001 From: Billy Rennekamp Date: Thu, 18 Jan 2024 11:28:09 +0100 Subject: [PATCH 03/20] re-rename --- .../DuckBurger_/DuckBurgerCounter_.js | 181 ----- .../DuckBurger_/DuckBurgerHQ_.js | 674 ------------------ .../DuckBurger_/DuckBurgerHQ_.sol | 407 ----------- .../DuckBurger_/DuckBurgerHQ_.yaml | 20 - .../DuckBurgerHQ.js => MOBA/MOBA.js} | 0 .../DuckBurgerHQ.sol => MOBA/MOBA.sol} | 32 +- .../DuckBurgerHQ.yaml => MOBA/MOBA.yaml} | 0 .../MOBACounter.js} | 0 8 files changed, 13 insertions(+), 1301 deletions(-) delete mode 100644 contracts/src/example-plugins/DuckBurger_/DuckBurgerCounter_.js delete mode 100644 contracts/src/example-plugins/DuckBurger_/DuckBurgerHQ_.js delete mode 100644 contracts/src/example-plugins/DuckBurger_/DuckBurgerHQ_.sol delete mode 100644 contracts/src/example-plugins/DuckBurger_/DuckBurgerHQ_.yaml rename contracts/src/example-plugins/{DuckBurger/DuckBurgerHQ.js => MOBA/MOBA.js} (100%) rename contracts/src/example-plugins/{DuckBurger/DuckBurgerHQ.sol => MOBA/MOBA.sol} (96%) rename contracts/src/example-plugins/{DuckBurger/DuckBurgerHQ.yaml => MOBA/MOBA.yaml} (100%) rename contracts/src/example-plugins/{DuckBurger/DuckBurgerCounter.js => MOBA/MOBACounter.js} (100%) diff --git a/contracts/src/example-plugins/DuckBurger_/DuckBurgerCounter_.js b/contracts/src/example-plugins/DuckBurger_/DuckBurgerCounter_.js deleted file mode 100644 index 905ffe3e5..000000000 --- a/contracts/src/example-plugins/DuckBurger_/DuckBurgerCounter_.js +++ /dev/null @@ -1,181 +0,0 @@ -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/example-plugins/DuckBurger_/DuckBurgerHQ_.js b/contracts/src/example-plugins/DuckBurger_/DuckBurgerHQ_.js deleted file mode 100644 index 72f3a25aa..000000000 --- a/contracts/src/example-plugins/DuckBurger_/DuckBurgerHQ_.js +++ /dev/null @@ -1,674 +0,0 @@ -import ds from "downstream"; - -const prizeFee = 2; -const prizeItemId = "0x6a7a67f063976de500000001000000010000000000000000"; // green goo -const buildingPrizeBagSlot = 0; -const buildingPrizeItemSlot = 0; -const nullBytes24 = `0x${"00".repeat(24)}`; -const duckBuildingTopId = "04"; -const burgerBuildingTopId = "17"; -const burgerCounterKindId = "Burger Display Building"; -const duckCounterKindId = "Duck Display Building"; -const countdownBuildingKindId = "Countdown Building"; - -let burgerCounter; -let duckCounter; -let countdownBuilding; -let startTime; -let endTime; - -export default async function update(state) { - - // - // Action handler functions - // - - // An action can set a form submit handler which will be called after the action along with the form values - let handleFormSubmit; - - const join = () => { - if (unitFeeBagSlot < 0) { - console.log( - "fee not found in bags - button should have been disabled", - ); - } - const mobileUnit = getMobileUnit(state); - - const payload = ds.encodeCall("function join()", []); - - const dummyBagIdIncaseToBagDoesNotExist = `0x${"00".repeat(24)}`; - - ds.dispatch( - { - name: "TRANSFER_ITEM_MOBILE_UNIT", - args: [ - mobileUnit.id, - [mobileUnit.id, selectedBuilding.id], - [unitFeeBagSlot, buildingPrizeBagSlot], - [unitFeeItemSlot, buildingPrizeItemSlot], - dummyBagIdIncaseToBagDoesNotExist, - prizeFee, - ], - }, - { - 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 selectedBuildingIdDuck = values["buildingKindIdDuck"]; - const selectedBuildingIdBurger = values["buildingKindIdBurger"]; - - console.log("start(): form.currentValues", values); - - // Verify selected buildings are different from each other - if (selectedBuildingIdDuck == selectedBuildingIdBurger) { - console.error("Team buildings must be different from each other", { - selectedBuildingIdDuck, - selectedBuildingIdBurger, - }); - return; - } - - const mobileUnit = getMobileUnit(state); - const payload = ds.encodeCall( - "function start(bytes24 duckBuildingID, bytes24 burgerBuildingID)", - [selectedBuildingIdDuck, selectedBuildingIdBurger], - ); - - ds.dispatch({ - name: "BUILDING_USE", - args: [selectedBuilding.id, mobileUnit.id, payload], - }); - }; - - const claim = () => { - const mobileUnit = getMobileUnit(state); - - const payload = ds.encodeCall("function claim()", []); - - 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 = "Duck Burger HQ"; - 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 DVB BUILDING FOUND"); - return { - version: 1, - map: [], - components: [ - { - id: "dbhq", - type: "building", - content: [ - { - id: "default", - type: "inline", - html: "", - buttons: [], - }, - ], - }, - ], - }; - } - - const { - prizePool, - gameActive, - startBlock, - endBlock, - buildingKindIdDuck, - buildingKindIdBurger, - teamDuckLength, - teamBurgerLength, - } = getHQData(selectedBuilding); - - const { unitFeeBagSlot, unitFeeItemSlot } = getMobileUnitFeeSlot(state); - const hasFee = unitFeeBagSlot >= 0; - const localBuildings = range5(state, selectedBuilding); - const duckCount = countBuildings( - localBuildings, - buildingKindIdDuck, - startBlock, - endBlock, - ); - const burgerCount = countBuildings( - localBuildings, - buildingKindIdBurger, - startBlock, - endBlock, - ); - - connectDisplayBuildings(state, localBuildings); - - - // 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 = "

Ducks vs Burgers HQ

"; - htmlBlock += `

payout for win: ${prizeFee * 2}

`; - htmlBlock += `

payout for draw: ${prizeFee}


`; - - - const canJoin = !gameActive && hasFee; - const canStart = !gameActive && teamDuckLength > 0 && teamBurgerLength > 0; - - if (canJoin) { - htmlBlock += `

total players: ${teamDuckLength + teamBurgerLength}


`; - } - - // Show what team the unit is on - const mobileUnit = getMobileUnit(state); - let isOnTeam = false; - if (mobileUnit){ - let unitTeam = ''; - - for (let i = 0; i < teamDuckLength; i++) { - if (mobileUnit.id == getHQTeamUnit(selectedBuilding, "Duck", i)) { - unitTeam = '🐤'; - break; - } - } - - if (unitTeam === '') { - for (let i = 0; i < teamBurgerLength; i++) { - if (mobileUnit.id == getHQTeamUnit(selectedBuilding, "Burger", i)) { - unitTeam = '🍔'; - break; - } - } - } - - if (unitTeam !== '') { - isOnTeam = true; - htmlBlock += ` -

You are on team ${unitTeam}


- `; - } - } - - if (!gameActive){ - if (!isOnTeam){ - buttonList.push({ - text: `Join Game (${prizeFee} Green Goo)`, - type: "action", - action: join, - disabled: !canJoin || isOnTeam, - }); - }else{ - // Check reason why game can't start - const waitingForStartCondition = teamDuckLength != teamBurgerLength || teamDuckLength + teamBurgerLength < 2; - let startConditionMessage = ""; - if (waitingForStartCondition){ - if (teamDuckLength + teamBurgerLength < 2){ - startConditionMessage = "Waiting for players..." - } else if (teamDuckLength != teamBurgerLength){ - startConditionMessage = "Teams must be balanced..."; - } - } - - buttonList.push({ - text: waitingForStartCondition ? startConditionMessage : "Start", - type: "action", - action: start, - disabled: !canStart || teamDuckLength != teamBurgerLength, - }); - } - } - - if (canStart) { - // Show options to select team buildings - htmlBlock += ` -

Select Team Buildings

-

Team 🐤

- ${getBuildingKindSelectHtml( - state, - duckBuildingTopId, - "buildingKindIdDuck", - )} -

Team 🍔

- ${getBuildingKindSelectHtml( - state, - burgerBuildingTopId, - "buildingKindIdBurger", - )} - `; - } - - const nowBlock = state?.world?.block; - const blocksLeft = endBlock > nowBlock ? endBlock - nowBlock : 0; - const blocksFromStart = startBlock < nowBlock ? nowBlock - startBlock : 30; - const timeLeftMs = blocksLeft * 2 * 1000; - const timeSinceStartMs = blocksFromStart * 2 * 1000; - - if (gameActive) { - // Display selected team buildings - const buildingKindDuck = - state.world.buildingKinds.find( - (b) => b.id === buildingKindIdDuck, - ) || {}; - const buildingKindBurger = - state.world.buildingKinds.find( - (b) => b.id === buildingKindIdBurger, - ) || {}; - htmlBlock += ` -

Team Buildings:

-

Team 🐤: ${buildingKindDuck.name?.value}

-

Team 🍔: ${buildingKindBurger.name?.value}


- - `; - - if (blocksLeft > 0) { - const now = Date.now(); - if (!startTime) startTime = now - timeSinceStartMs; - if (!endTime) endTime = now + timeLeftMs; - htmlBlock += `

time remaining: ${formatTime(timeLeftMs)}

`; - } else { - // End of game - buttonList.push({ - text: prizePool > 0 ? `Claim Reward` : "Nothing to Claim", - type: "action", - action: claim, - disabled: prizePool == 0, - }); - - htmlBlock += ` -

Game Over:

-

Final Score: 🐤${duckCount} : 🍔${burgerCount} - `; - if (duckCount == burgerCount) { - htmlBlock += ` -

The result was a draw

- `; - } else { - const winningTeamName = - duckCount > burgerCount ? "duck" : "burger"; - const winningTeamEmoji = duckCount > burgerCount ? "🐤" : "🍔"; - htmlBlock += ` -

Team ${winningTeamName} have won the match!

-

${winningTeamEmoji}🏆

- `; - } - } - } else { - startTime = undefined; - endTime = undefined; - } - - // Reset is always offered (requires some trust!) - buttonList.push({ - text: "Reset", - type: "action", - action: reset, - disabled: false, - }); - - // build up an array o fmap objects which are used to update display buildings - // always show the current team counts - const mapObj = [ - { - type: "building", - id: `${burgerCounter ? burgerCounter.id : ""}`, - key: "labelText", - value: `${burgerCount}`, - }, - { - type: "building", - id: `${duckCounter ? duckCounter.id : ""}`, - key: "labelText", - value: `${duckCount}`, - }, - ]; - - // if the game is running show the time - if (gameActive && blocksLeft > 0) { - mapObj.push( - { - type: "building", - id: `${countdownBuilding ? countdownBuilding.id : ""}`, - key: "countdown-start", - value: `${startTime}`, - }, - { - type: "building", - id: `${countdownBuilding ? countdownBuilding.id : ""}`, - key: "countdown-end", - value: `${endTime}`, - }, - ); - } else { - mapObj.push({ - type: "building", - id: `${countdownBuilding ? countdownBuilding.id : ""}`, - key: "labelText", - value: "", - }); - } - - return { - version: 1, - map: mapObj, - components: [ - { - id: "dbhq", - type: "building", - content: [ - { - id: "default", - type: "inline", - html: htmlBlock, - submit: (values) => { - if (typeof handleFormSubmit == "function") { - handleFormSubmit(values); - } - }, - buttons: buttonList, - }, - ], - }, - ], - }; -} - -// --- Duckbur HQ Specific functions - -function getHQData(selectedBuilding) { - const prizePool = getDataInt(selectedBuilding, "prizePool"); - const gameActive = getDataBool(selectedBuilding, "gameActive"); - const startBlock = getDataInt(selectedBuilding, "startBlock"); - const endBlock = getDataInt(selectedBuilding, "endBlock"); - const buildingKindIdDuck = getDataBytes24( - selectedBuilding, - "buildingKindIdDuck", - ); - const buildingKindIdBurger = getDataBytes24( - selectedBuilding, - "buildingKindIdBurger", - ); - const teamDuckLength = getDataInt(selectedBuilding, "teamDuckLength"); - const teamBurgerLength = getDataInt(selectedBuilding, "teamBurgerLength"); - - return { - prizePool, - gameActive, - startBlock, - endBlock, - startBlock, - buildingKindIdDuck, - buildingKindIdBurger, - teamDuckLength, - teamBurgerLength, - }; -} - -function getHQTeamUnit(selectedBuilding, team, index){ - return getDataBytes24(selectedBuilding, `team${team}Unit_${index}`); -} - -// search the buildings list ofr the display buildings we're gpoing to use -// for team counts and coutdown -function connectDisplayBuildings(state, buildings) { - if (!burgerCounter) { - burgerCounter = buildings.find((element) => - getBuildingKindsByTileLocation(state, element, burgerCounterKindId), - ); - } - if (!duckCounter) { - duckCounter = buildings.find((element) => - getBuildingKindsByTileLocation(state, element, duckCounterKindId), - ); - } - if (!countdownBuilding) { - countdownBuilding = buildings.find((element) => - getBuildingKindsByTileLocation( - state, - element, - countdownBuildingKindId, - ), - ); - } -} - -function formatTime(timeInMs) { - let seconds = Math.floor(timeInMs / 1000); - let minutes = Math.floor(seconds / 60); - let hours = Math.floor(minutes / 60); - - seconds %= 60; - minutes %= 60; - - // Pad each component to ensure two digits - let formattedHours = String(hours).padStart(2, "0"); - let formattedMinutes = String(minutes).padStart(2, "0"); - let formattedSeconds = String(seconds).padStart(2, "0"); - - return `${formattedHours}:${formattedMinutes}:${formattedSeconds}`; -} - -const countBuildings = (buildingsArray, kindID, startBlock, endBlock) => { - return buildingsArray.filter( - (b) => - b.kind?.id == kindID && - b.constructionBlockNum.value >= startBlock && - b.constructionBlockNum.value <= endBlock, - ).length; -}; - -function getMobileUnitFeeSlot(state) { - const mobileUnit = getMobileUnit(state); - const mobileUnitBags = mobileUnit ? getEquipeeBags(state, mobileUnit) : []; - const { bag, slotKey } = findBagAndSlot( - mobileUnitBags, - prizeItemId, - prizeFee, - ); - const unitFeeBagSlot = bag ? bag.equipee.key : -1; - const unitFeeItemSlot = bag ? slotKey : -1; - return { - unitFeeBagSlot, - unitFeeItemSlot, - }; -} - -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]), - ); -} - -function getBuildingKindsByTileLocation(state, building, kindID) { - return (state?.world?.buildings || []).find( - (b) => b.id === building.id && b.kind?.name?.value == kindID, - ); -} - -// get first slot in bags that matches item requirements -function findBagAndSlot(bags, requiredItemId, requiredBalance) { - for (const bag of bags) { - for (const slotKey in bag.slots) { - const slot = bag.slots[slotKey]; - if ( - (!requiredItemId || slot.item.id == requiredItemId) && - requiredBalance <= slot.balance - ) { - return { - bag: bag, - slotKey: slot.key, // assuming each slot has a 'key' property - }; - } - } - } - return { bag: null, slotKey: -1 }; -} - -// -- 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; - }, {}); -} - -// 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/DuckBurger_/DuckBurgerHQ_.sol b/contracts/src/example-plugins/DuckBurger_/DuckBurgerHQ_.sol deleted file mode 100644 index 75b20c850..000000000 --- a/contracts/src/example-plugins/DuckBurger_/DuckBurgerHQ_.sol +++ /dev/null @@ -1,407 +0,0 @@ -// 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 DuckBurgerHQ is BuildingKind { - // todo - storing contract members like this is per BuildingKind - // to work with building instances and therefore allow multiple buildings - // this data should be stored either as a map to buildingInstance - // or only use SET_DATA_ON_BUILDING action - bytes24[] private teamDuckUnits; - bytes24[] private teamBurgerUnits; - - // consts - // prize bag info - uint8 constant prizeBagSlot = 0; - uint8 constant prizeItemSlot = 0; - uint64 constant joinFee = 2; - - // 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 { - 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); - } else if ((bytes4)(payload) == this.claim.selector) { - _claim(ds, state, actor, buildingInstance); - } else if ((bytes4)(payload) == this.reset.selector) { - _reset(ds, buildingInstance); - } - - ds.getDispatcher().dispatch( - abi.encodeCall( - Actions.SET_DATA_ON_BUILDING, (buildingInstance, "prizePool", bytes32(uint256(_calculatePool()))) - ) - ); - } - - 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) { - revert("Can't join while a game is already active"); - } - - // 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 currentPrizeBalance = _getPrizeBalance(state, buildingId); - if ((currentPrizeBalance - joinFee) < lastKnownPrizeBalance) { - revert("Fee not paid"); - } - - // remember the new balance - Dispatcher dispatcher = ds.getDispatcher(); - dispatcher.dispatch( - abi.encodeCall( - Actions.SET_DATA_ON_BUILDING, - (buildingId, "lastKnownPrizeBalance", bytes32(uint256(currentPrizeBalance))) - ) - ); - - for (uint256 i = 0; i < teamDuckUnits.length; i++) { - if (teamDuckUnits[i] == unitId) revert("Already joined"); - } - for (uint256 i = 0; i < teamBurgerUnits.length; i++) { - if (teamBurgerUnits[i] == unitId) revert("Already joined"); - } - - // Assign a team - if (teamDuckUnits.length <= teamBurgerUnits.length) { - teamDuckUnits.push(unitId); - assignUnitToTeam(ds, "duck", unitId, buildingId); - } else { - teamBurgerUnits.push(unitId); - assignUnitToTeam(ds, "burger", 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("duck"))) { - processTeam(dispatcher, buildingId, "teamDuck", teamDuckUnits, unitId); - } else if (keccak256(abi.encodePacked(team)) == keccak256(abi.encodePacked("burger"))) { - processTeam(dispatcher, buildingId, "teamBurger", teamBurgerUnits, unitId); - } - } - - 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 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")); - 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))) - ); - dispatcher.dispatch( - abi.encodeCall( - 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)))) - ); - - // 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))) - ) - ); - - // set start to now - dispatcher.dispatch( - 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)))) - ); - } - - function _claim(Game ds, State state, bytes24 unitId, bytes24 buildingId) private { - // check game finished - { - uint256 endBlock = uint256(state.getData(buildingId, "endBlock")); - if (block.number < endBlock) { - revert("Can't claim, game is running"); - } - } - - // check unit in a team - // check unit not already claimed - bool isDuckTeamMember = false; - bool isBurgerTeamMember = false; - for (uint256 i = 0; i < teamDuckUnits.length; i++) { - if (teamDuckUnits[i] == unitId) { - isDuckTeamMember = true; - break; - } - } - for (uint256 i = 0; i < teamBurgerUnits.length; i++) { - if (teamBurgerUnits[i] == unitId) { - isBurgerTeamMember = true; - break; - } - } - 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); - - // check unit is in winning team - - if (isDuckTeamMember && duckBuildings < burgerBuildings) { - revert("You, duck, are not on the winning team: burgers"); - } else if (isBurgerTeamMember && burgerBuildings < duckBuildings) { - revert("You, burger, are not on the winning team: ducks"); - } - isDraw = burgerBuildings == duckBuildings; - } - - // winner! (or drawer) - // \todo this currently assumes even teams - Dispatcher dispatcher = ds.getDispatcher(); - _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)))) - ) - ); - - // Remove unit from team so they can't double claim - if (isDuckTeamMember) { - removeUnitFromArray(teamDuckUnits, unitId); - } else if (isBurgerTeamMember) { - removeUnitFromArray(teamBurgerUnits, unitId); - } - } - - 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); - - (uint8 destBagSlot, uint8 destItemSlot) = _findValidItemSlot(state, unitId, prizeItemId, prizeAmount); - - dispatcher.dispatch( - abi.encodeCall( - Actions.TRANSFER_ITEM_MOBILE_UNIT, - ( - buildingId, - [buildingId, unitId], - [prizeBagSlot, destBagSlot], - [prizeItemSlot, destItemSlot], - bytes24(0), // To bag ID not required - prizeAmount - ) - ) - ); - } - - 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"); - - for (destItemSlot = 0; destItemSlot < 4; destItemSlot++) { - (bytes24 destItemId, uint64 destBalance) = state.getItemSlot(destBagId, destItemSlot); - if ((destItemId == bytes24(0) || destItemId == itemId) && destBalance + transferAmount <= 100) { - // Found valid slot - return (destBagSlot, destItemSlot); - } - } - } - - revert("No valid slot for prize claim found"); - } - - 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 :) - - // set state to joining (gameActive ?) - dispatcher.dispatch( - 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)))) - ); - dispatcher.dispatch( - 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))) - ); - 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) { - bytes24 prizeBag = state.getEquipSlot(buildingId, prizeBagSlot); - (, uint64 balance) = state.getItemSlot(prizeBag, prizeItemSlot); - return balance; - } - - function _calculatePrizeAmount() internal pure returns (uint64) { - return joinFee * 2; - } - - function _calculatePool() internal view returns (uint64) { - 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")); - uint256 endBlock = uint256(state.getData(buildingInstance, "endBlock")); - 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] - ); - if (state.getBuildingKind(arenaBuildingID) == duckBuildingKind) { - 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) { - burgers++; - } - } - } - } - - 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/DuckBurger_/DuckBurgerHQ_.yaml b/contracts/src/example-plugins/DuckBurger_/DuckBurgerHQ_.yaml deleted file mode 100644 index 5eb304abd..000000000 --- a/contracts/src/example-plugins/DuckBurger_/DuckBurgerHQ_.yaml +++ /dev/null @@ -1,20 +0,0 @@ - -kind: BuildingKind -spec: - name: Duck Burger HQ - description: "Play Ducks vs Burgers" - category: custom - model: 11-03 - color: 1 - contract: - file: ./DuckBurgerHQ.sol - plugin: - file: ./DuckBurgerHQ.js - alwaysActive: true - materials: - - name: Green Goo - quantity: 10 - - name: Blue Goo - quantity: 10 - - name: Red Goo - quantity: 10 \ No newline at end of file diff --git a/contracts/src/example-plugins/DuckBurger/DuckBurgerHQ.js b/contracts/src/example-plugins/MOBA/MOBA.js similarity index 100% rename from contracts/src/example-plugins/DuckBurger/DuckBurgerHQ.js rename to contracts/src/example-plugins/MOBA/MOBA.js diff --git a/contracts/src/example-plugins/DuckBurger/DuckBurgerHQ.sol b/contracts/src/example-plugins/MOBA/MOBA.sol similarity index 96% rename from contracts/src/example-plugins/DuckBurger/DuckBurgerHQ.sol rename to contracts/src/example-plugins/MOBA/MOBA.sol index 9b26e1ef2..8869556e1 100644 --- a/contracts/src/example-plugins/DuckBurger/DuckBurgerHQ.sol +++ b/contracts/src/example-plugins/MOBA/MOBA.sol @@ -31,7 +31,7 @@ contract MOBA is BuildingKind { // these functions do not have their own definitions function join() external {} - function start(bytes24 duckBuildingID, bytes24 burgerBuildingID) external {} + function start(bytes24 redBaseID, bytes24 blueBaseID) external {} function claim() external {} @@ -49,33 +49,27 @@ contract MOBA is BuildingKind { if ((bytes4)(payload) == this.join.selector) { _join(ds, state, actor, buildingInstance); } else if ((bytes4)(payload) == this.start.selector) { - (bytes24 duckBuildingID, bytes24 burgerBuildingID) = abi.decode( + (bytes24 redBaseID, bytes24 blueBaseId) = abi.decode( payload[4:], (bytes24, bytes24) ); - _start( - ds, - state, - buildingInstance, - duckBuildingID, - burgerBuildingID - ); + _start(ds, state, buildingInstance, redBaseID, blueBaseId); } else if ((bytes4)(payload) == this.claim.selector) { _claim(ds, state, actor, buildingInstance); } else if ((bytes4)(payload) == this.reset.selector) { _reset(ds, buildingInstance); } - ds.getDispatcher().dispatch( - abi.encodeCall( - Actions.SET_DATA_ON_BUILDING, - ( - buildingInstance, - "prizePool", - bytes32(uint256(_calculatePool())) - ) - ) - ); + // ds.getDispatcher().dispatch( + // abi.encodeCall( + // Actions.SET_DATA_ON_BUILDING, + // ( + // buildingInstance, + // "prizePool", + // bytes32(uint256(_calculatePool())) + // ) + // ) + // ); } function _join( diff --git a/contracts/src/example-plugins/DuckBurger/DuckBurgerHQ.yaml b/contracts/src/example-plugins/MOBA/MOBA.yaml similarity index 100% rename from contracts/src/example-plugins/DuckBurger/DuckBurgerHQ.yaml rename to contracts/src/example-plugins/MOBA/MOBA.yaml diff --git a/contracts/src/example-plugins/DuckBurger/DuckBurgerCounter.js b/contracts/src/example-plugins/MOBA/MOBACounter.js similarity index 100% rename from contracts/src/example-plugins/DuckBurger/DuckBurgerCounter.js rename to contracts/src/example-plugins/MOBA/MOBACounter.js From 397c4bbc646fe3266e7a2481aa92213051730f8a Mon Sep 17 00:00:00 2001 From: Billy Rennekamp Date: Thu, 18 Jan 2024 11:30:13 +0100 Subject: [PATCH 04/20] add back duckburger --- .../DuckBurger/DuckBurgerCounter.js | 181 +++++ .../DuckBurger/DuckBurgerHQ.js | 674 ++++++++++++++++++ .../DuckBurger/DuckBurgerHQ.sol | 407 +++++++++++ .../DuckBurger/DuckBurgerHQ.yaml | 20 + 4 files changed, 1282 insertions(+) create mode 100644 contracts/src/example-plugins/DuckBurger/DuckBurgerCounter.js create mode 100644 contracts/src/example-plugins/DuckBurger/DuckBurgerHQ.js create mode 100644 contracts/src/example-plugins/DuckBurger/DuckBurgerHQ.sol create mode 100644 contracts/src/example-plugins/DuckBurger/DuckBurgerHQ.yaml diff --git a/contracts/src/example-plugins/DuckBurger/DuckBurgerCounter.js b/contracts/src/example-plugins/DuckBurger/DuckBurgerCounter.js new file mode 100644 index 000000000..905ffe3e5 --- /dev/null +++ b/contracts/src/example-plugins/DuckBurger/DuckBurgerCounter.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/example-plugins/DuckBurger/DuckBurgerHQ.js b/contracts/src/example-plugins/DuckBurger/DuckBurgerHQ.js new file mode 100644 index 000000000..72f3a25aa --- /dev/null +++ b/contracts/src/example-plugins/DuckBurger/DuckBurgerHQ.js @@ -0,0 +1,674 @@ +import ds from "downstream"; + +const prizeFee = 2; +const prizeItemId = "0x6a7a67f063976de500000001000000010000000000000000"; // green goo +const buildingPrizeBagSlot = 0; +const buildingPrizeItemSlot = 0; +const nullBytes24 = `0x${"00".repeat(24)}`; +const duckBuildingTopId = "04"; +const burgerBuildingTopId = "17"; +const burgerCounterKindId = "Burger Display Building"; +const duckCounterKindId = "Duck Display Building"; +const countdownBuildingKindId = "Countdown Building"; + +let burgerCounter; +let duckCounter; +let countdownBuilding; +let startTime; +let endTime; + +export default async function update(state) { + + // + // Action handler functions + // + + // An action can set a form submit handler which will be called after the action along with the form values + let handleFormSubmit; + + const join = () => { + if (unitFeeBagSlot < 0) { + console.log( + "fee not found in bags - button should have been disabled", + ); + } + const mobileUnit = getMobileUnit(state); + + const payload = ds.encodeCall("function join()", []); + + const dummyBagIdIncaseToBagDoesNotExist = `0x${"00".repeat(24)}`; + + ds.dispatch( + { + name: "TRANSFER_ITEM_MOBILE_UNIT", + args: [ + mobileUnit.id, + [mobileUnit.id, selectedBuilding.id], + [unitFeeBagSlot, buildingPrizeBagSlot], + [unitFeeItemSlot, buildingPrizeItemSlot], + dummyBagIdIncaseToBagDoesNotExist, + prizeFee, + ], + }, + { + 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 selectedBuildingIdDuck = values["buildingKindIdDuck"]; + const selectedBuildingIdBurger = values["buildingKindIdBurger"]; + + console.log("start(): form.currentValues", values); + + // Verify selected buildings are different from each other + if (selectedBuildingIdDuck == selectedBuildingIdBurger) { + console.error("Team buildings must be different from each other", { + selectedBuildingIdDuck, + selectedBuildingIdBurger, + }); + return; + } + + const mobileUnit = getMobileUnit(state); + const payload = ds.encodeCall( + "function start(bytes24 duckBuildingID, bytes24 burgerBuildingID)", + [selectedBuildingIdDuck, selectedBuildingIdBurger], + ); + + ds.dispatch({ + name: "BUILDING_USE", + args: [selectedBuilding.id, mobileUnit.id, payload], + }); + }; + + const claim = () => { + const mobileUnit = getMobileUnit(state); + + const payload = ds.encodeCall("function claim()", []); + + 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 = "Duck Burger HQ"; + 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 DVB BUILDING FOUND"); + return { + version: 1, + map: [], + components: [ + { + id: "dbhq", + type: "building", + content: [ + { + id: "default", + type: "inline", + html: "", + buttons: [], + }, + ], + }, + ], + }; + } + + const { + prizePool, + gameActive, + startBlock, + endBlock, + buildingKindIdDuck, + buildingKindIdBurger, + teamDuckLength, + teamBurgerLength, + } = getHQData(selectedBuilding); + + const { unitFeeBagSlot, unitFeeItemSlot } = getMobileUnitFeeSlot(state); + const hasFee = unitFeeBagSlot >= 0; + const localBuildings = range5(state, selectedBuilding); + const duckCount = countBuildings( + localBuildings, + buildingKindIdDuck, + startBlock, + endBlock, + ); + const burgerCount = countBuildings( + localBuildings, + buildingKindIdBurger, + startBlock, + endBlock, + ); + + connectDisplayBuildings(state, localBuildings); + + + // 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 = "

Ducks vs Burgers HQ

"; + htmlBlock += `

payout for win: ${prizeFee * 2}

`; + htmlBlock += `

payout for draw: ${prizeFee}


`; + + + const canJoin = !gameActive && hasFee; + const canStart = !gameActive && teamDuckLength > 0 && teamBurgerLength > 0; + + if (canJoin) { + htmlBlock += `

total players: ${teamDuckLength + teamBurgerLength}


`; + } + + // Show what team the unit is on + const mobileUnit = getMobileUnit(state); + let isOnTeam = false; + if (mobileUnit){ + let unitTeam = ''; + + for (let i = 0; i < teamDuckLength; i++) { + if (mobileUnit.id == getHQTeamUnit(selectedBuilding, "Duck", i)) { + unitTeam = '🐤'; + break; + } + } + + if (unitTeam === '') { + for (let i = 0; i < teamBurgerLength; i++) { + if (mobileUnit.id == getHQTeamUnit(selectedBuilding, "Burger", i)) { + unitTeam = '🍔'; + break; + } + } + } + + if (unitTeam !== '') { + isOnTeam = true; + htmlBlock += ` +

You are on team ${unitTeam}


+ `; + } + } + + if (!gameActive){ + if (!isOnTeam){ + buttonList.push({ + text: `Join Game (${prizeFee} Green Goo)`, + type: "action", + action: join, + disabled: !canJoin || isOnTeam, + }); + }else{ + // Check reason why game can't start + const waitingForStartCondition = teamDuckLength != teamBurgerLength || teamDuckLength + teamBurgerLength < 2; + let startConditionMessage = ""; + if (waitingForStartCondition){ + if (teamDuckLength + teamBurgerLength < 2){ + startConditionMessage = "Waiting for players..." + } else if (teamDuckLength != teamBurgerLength){ + startConditionMessage = "Teams must be balanced..."; + } + } + + buttonList.push({ + text: waitingForStartCondition ? startConditionMessage : "Start", + type: "action", + action: start, + disabled: !canStart || teamDuckLength != teamBurgerLength, + }); + } + } + + if (canStart) { + // Show options to select team buildings + htmlBlock += ` +

Select Team Buildings

+

Team 🐤

+ ${getBuildingKindSelectHtml( + state, + duckBuildingTopId, + "buildingKindIdDuck", + )} +

Team 🍔

+ ${getBuildingKindSelectHtml( + state, + burgerBuildingTopId, + "buildingKindIdBurger", + )} + `; + } + + const nowBlock = state?.world?.block; + const blocksLeft = endBlock > nowBlock ? endBlock - nowBlock : 0; + const blocksFromStart = startBlock < nowBlock ? nowBlock - startBlock : 30; + const timeLeftMs = blocksLeft * 2 * 1000; + const timeSinceStartMs = blocksFromStart * 2 * 1000; + + if (gameActive) { + // Display selected team buildings + const buildingKindDuck = + state.world.buildingKinds.find( + (b) => b.id === buildingKindIdDuck, + ) || {}; + const buildingKindBurger = + state.world.buildingKinds.find( + (b) => b.id === buildingKindIdBurger, + ) || {}; + htmlBlock += ` +

Team Buildings:

+

Team 🐤: ${buildingKindDuck.name?.value}

+

Team 🍔: ${buildingKindBurger.name?.value}


+ + `; + + if (blocksLeft > 0) { + const now = Date.now(); + if (!startTime) startTime = now - timeSinceStartMs; + if (!endTime) endTime = now + timeLeftMs; + htmlBlock += `

time remaining: ${formatTime(timeLeftMs)}

`; + } else { + // End of game + buttonList.push({ + text: prizePool > 0 ? `Claim Reward` : "Nothing to Claim", + type: "action", + action: claim, + disabled: prizePool == 0, + }); + + htmlBlock += ` +

Game Over:

+

Final Score: 🐤${duckCount} : 🍔${burgerCount} + `; + if (duckCount == burgerCount) { + htmlBlock += ` +

The result was a draw

+ `; + } else { + const winningTeamName = + duckCount > burgerCount ? "duck" : "burger"; + const winningTeamEmoji = duckCount > burgerCount ? "🐤" : "🍔"; + htmlBlock += ` +

Team ${winningTeamName} have won the match!

+

${winningTeamEmoji}🏆

+ `; + } + } + } else { + startTime = undefined; + endTime = undefined; + } + + // Reset is always offered (requires some trust!) + buttonList.push({ + text: "Reset", + type: "action", + action: reset, + disabled: false, + }); + + // build up an array o fmap objects which are used to update display buildings + // always show the current team counts + const mapObj = [ + { + type: "building", + id: `${burgerCounter ? burgerCounter.id : ""}`, + key: "labelText", + value: `${burgerCount}`, + }, + { + type: "building", + id: `${duckCounter ? duckCounter.id : ""}`, + key: "labelText", + value: `${duckCount}`, + }, + ]; + + // if the game is running show the time + if (gameActive && blocksLeft > 0) { + mapObj.push( + { + type: "building", + id: `${countdownBuilding ? countdownBuilding.id : ""}`, + key: "countdown-start", + value: `${startTime}`, + }, + { + type: "building", + id: `${countdownBuilding ? countdownBuilding.id : ""}`, + key: "countdown-end", + value: `${endTime}`, + }, + ); + } else { + mapObj.push({ + type: "building", + id: `${countdownBuilding ? countdownBuilding.id : ""}`, + key: "labelText", + value: "", + }); + } + + return { + version: 1, + map: mapObj, + components: [ + { + id: "dbhq", + type: "building", + content: [ + { + id: "default", + type: "inline", + html: htmlBlock, + submit: (values) => { + if (typeof handleFormSubmit == "function") { + handleFormSubmit(values); + } + }, + buttons: buttonList, + }, + ], + }, + ], + }; +} + +// --- Duckbur HQ Specific functions + +function getHQData(selectedBuilding) { + const prizePool = getDataInt(selectedBuilding, "prizePool"); + const gameActive = getDataBool(selectedBuilding, "gameActive"); + const startBlock = getDataInt(selectedBuilding, "startBlock"); + const endBlock = getDataInt(selectedBuilding, "endBlock"); + const buildingKindIdDuck = getDataBytes24( + selectedBuilding, + "buildingKindIdDuck", + ); + const buildingKindIdBurger = getDataBytes24( + selectedBuilding, + "buildingKindIdBurger", + ); + const teamDuckLength = getDataInt(selectedBuilding, "teamDuckLength"); + const teamBurgerLength = getDataInt(selectedBuilding, "teamBurgerLength"); + + return { + prizePool, + gameActive, + startBlock, + endBlock, + startBlock, + buildingKindIdDuck, + buildingKindIdBurger, + teamDuckLength, + teamBurgerLength, + }; +} + +function getHQTeamUnit(selectedBuilding, team, index){ + return getDataBytes24(selectedBuilding, `team${team}Unit_${index}`); +} + +// search the buildings list ofr the display buildings we're gpoing to use +// for team counts and coutdown +function connectDisplayBuildings(state, buildings) { + if (!burgerCounter) { + burgerCounter = buildings.find((element) => + getBuildingKindsByTileLocation(state, element, burgerCounterKindId), + ); + } + if (!duckCounter) { + duckCounter = buildings.find((element) => + getBuildingKindsByTileLocation(state, element, duckCounterKindId), + ); + } + if (!countdownBuilding) { + countdownBuilding = buildings.find((element) => + getBuildingKindsByTileLocation( + state, + element, + countdownBuildingKindId, + ), + ); + } +} + +function formatTime(timeInMs) { + let seconds = Math.floor(timeInMs / 1000); + let minutes = Math.floor(seconds / 60); + let hours = Math.floor(minutes / 60); + + seconds %= 60; + minutes %= 60; + + // Pad each component to ensure two digits + let formattedHours = String(hours).padStart(2, "0"); + let formattedMinutes = String(minutes).padStart(2, "0"); + let formattedSeconds = String(seconds).padStart(2, "0"); + + return `${formattedHours}:${formattedMinutes}:${formattedSeconds}`; +} + +const countBuildings = (buildingsArray, kindID, startBlock, endBlock) => { + return buildingsArray.filter( + (b) => + b.kind?.id == kindID && + b.constructionBlockNum.value >= startBlock && + b.constructionBlockNum.value <= endBlock, + ).length; +}; + +function getMobileUnitFeeSlot(state) { + const mobileUnit = getMobileUnit(state); + const mobileUnitBags = mobileUnit ? getEquipeeBags(state, mobileUnit) : []; + const { bag, slotKey } = findBagAndSlot( + mobileUnitBags, + prizeItemId, + prizeFee, + ); + const unitFeeBagSlot = bag ? bag.equipee.key : -1; + const unitFeeItemSlot = bag ? slotKey : -1; + return { + unitFeeBagSlot, + unitFeeItemSlot, + }; +} + +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]), + ); +} + +function getBuildingKindsByTileLocation(state, building, kindID) { + return (state?.world?.buildings || []).find( + (b) => b.id === building.id && b.kind?.name?.value == kindID, + ); +} + +// get first slot in bags that matches item requirements +function findBagAndSlot(bags, requiredItemId, requiredBalance) { + for (const bag of bags) { + for (const slotKey in bag.slots) { + const slot = bag.slots[slotKey]; + if ( + (!requiredItemId || slot.item.id == requiredItemId) && + requiredBalance <= slot.balance + ) { + return { + bag: bag, + slotKey: slot.key, // assuming each slot has a 'key' property + }; + } + } + } + return { bag: null, slotKey: -1 }; +} + +// -- 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; + }, {}); +} + +// 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/DuckBurger/DuckBurgerHQ.sol b/contracts/src/example-plugins/DuckBurger/DuckBurgerHQ.sol new file mode 100644 index 000000000..75b20c850 --- /dev/null +++ b/contracts/src/example-plugins/DuckBurger/DuckBurgerHQ.sol @@ -0,0 +1,407 @@ +// 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 DuckBurgerHQ is BuildingKind { + // todo - storing contract members like this is per BuildingKind + // to work with building instances and therefore allow multiple buildings + // this data should be stored either as a map to buildingInstance + // or only use SET_DATA_ON_BUILDING action + bytes24[] private teamDuckUnits; + bytes24[] private teamBurgerUnits; + + // consts + // prize bag info + uint8 constant prizeBagSlot = 0; + uint8 constant prizeItemSlot = 0; + uint64 constant joinFee = 2; + + // 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 { + 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); + } else if ((bytes4)(payload) == this.claim.selector) { + _claim(ds, state, actor, buildingInstance); + } else if ((bytes4)(payload) == this.reset.selector) { + _reset(ds, buildingInstance); + } + + ds.getDispatcher().dispatch( + abi.encodeCall( + Actions.SET_DATA_ON_BUILDING, (buildingInstance, "prizePool", bytes32(uint256(_calculatePool()))) + ) + ); + } + + 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) { + revert("Can't join while a game is already active"); + } + + // 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 currentPrizeBalance = _getPrizeBalance(state, buildingId); + if ((currentPrizeBalance - joinFee) < lastKnownPrizeBalance) { + revert("Fee not paid"); + } + + // remember the new balance + Dispatcher dispatcher = ds.getDispatcher(); + dispatcher.dispatch( + abi.encodeCall( + Actions.SET_DATA_ON_BUILDING, + (buildingId, "lastKnownPrizeBalance", bytes32(uint256(currentPrizeBalance))) + ) + ); + + for (uint256 i = 0; i < teamDuckUnits.length; i++) { + if (teamDuckUnits[i] == unitId) revert("Already joined"); + } + for (uint256 i = 0; i < teamBurgerUnits.length; i++) { + if (teamBurgerUnits[i] == unitId) revert("Already joined"); + } + + // Assign a team + if (teamDuckUnits.length <= teamBurgerUnits.length) { + teamDuckUnits.push(unitId); + assignUnitToTeam(ds, "duck", unitId, buildingId); + } else { + teamBurgerUnits.push(unitId); + assignUnitToTeam(ds, "burger", 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("duck"))) { + processTeam(dispatcher, buildingId, "teamDuck", teamDuckUnits, unitId); + } else if (keccak256(abi.encodePacked(team)) == keccak256(abi.encodePacked("burger"))) { + processTeam(dispatcher, buildingId, "teamBurger", teamBurgerUnits, unitId); + } + } + + 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 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")); + 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))) + ); + dispatcher.dispatch( + abi.encodeCall( + 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)))) + ); + + // 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))) + ) + ); + + // set start to now + dispatcher.dispatch( + 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)))) + ); + } + + function _claim(Game ds, State state, bytes24 unitId, bytes24 buildingId) private { + // check game finished + { + uint256 endBlock = uint256(state.getData(buildingId, "endBlock")); + if (block.number < endBlock) { + revert("Can't claim, game is running"); + } + } + + // check unit in a team + // check unit not already claimed + bool isDuckTeamMember = false; + bool isBurgerTeamMember = false; + for (uint256 i = 0; i < teamDuckUnits.length; i++) { + if (teamDuckUnits[i] == unitId) { + isDuckTeamMember = true; + break; + } + } + for (uint256 i = 0; i < teamBurgerUnits.length; i++) { + if (teamBurgerUnits[i] == unitId) { + isBurgerTeamMember = true; + break; + } + } + 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); + + // check unit is in winning team + + if (isDuckTeamMember && duckBuildings < burgerBuildings) { + revert("You, duck, are not on the winning team: burgers"); + } else if (isBurgerTeamMember && burgerBuildings < duckBuildings) { + revert("You, burger, are not on the winning team: ducks"); + } + isDraw = burgerBuildings == duckBuildings; + } + + // winner! (or drawer) + // \todo this currently assumes even teams + Dispatcher dispatcher = ds.getDispatcher(); + _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)))) + ) + ); + + // Remove unit from team so they can't double claim + if (isDuckTeamMember) { + removeUnitFromArray(teamDuckUnits, unitId); + } else if (isBurgerTeamMember) { + removeUnitFromArray(teamBurgerUnits, unitId); + } + } + + 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); + + (uint8 destBagSlot, uint8 destItemSlot) = _findValidItemSlot(state, unitId, prizeItemId, prizeAmount); + + dispatcher.dispatch( + abi.encodeCall( + Actions.TRANSFER_ITEM_MOBILE_UNIT, + ( + buildingId, + [buildingId, unitId], + [prizeBagSlot, destBagSlot], + [prizeItemSlot, destItemSlot], + bytes24(0), // To bag ID not required + prizeAmount + ) + ) + ); + } + + 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"); + + for (destItemSlot = 0; destItemSlot < 4; destItemSlot++) { + (bytes24 destItemId, uint64 destBalance) = state.getItemSlot(destBagId, destItemSlot); + if ((destItemId == bytes24(0) || destItemId == itemId) && destBalance + transferAmount <= 100) { + // Found valid slot + return (destBagSlot, destItemSlot); + } + } + } + + revert("No valid slot for prize claim found"); + } + + 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 :) + + // set state to joining (gameActive ?) + dispatcher.dispatch( + 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)))) + ); + dispatcher.dispatch( + 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))) + ); + 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) { + bytes24 prizeBag = state.getEquipSlot(buildingId, prizeBagSlot); + (, uint64 balance) = state.getItemSlot(prizeBag, prizeItemSlot); + return balance; + } + + function _calculatePrizeAmount() internal pure returns (uint64) { + return joinFee * 2; + } + + function _calculatePool() internal view returns (uint64) { + 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")); + uint256 endBlock = uint256(state.getData(buildingInstance, "endBlock")); + 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] + ); + if (state.getBuildingKind(arenaBuildingID) == duckBuildingKind) { + 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) { + burgers++; + } + } + } + } + + 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/DuckBurger/DuckBurgerHQ.yaml b/contracts/src/example-plugins/DuckBurger/DuckBurgerHQ.yaml new file mode 100644 index 000000000..5eb304abd --- /dev/null +++ b/contracts/src/example-plugins/DuckBurger/DuckBurgerHQ.yaml @@ -0,0 +1,20 @@ + +kind: BuildingKind +spec: + name: Duck Burger HQ + description: "Play Ducks vs Burgers" + category: custom + model: 11-03 + color: 1 + contract: + file: ./DuckBurgerHQ.sol + plugin: + file: ./DuckBurgerHQ.js + alwaysActive: true + materials: + - name: Green Goo + quantity: 10 + - name: Blue Goo + quantity: 10 + - name: Red Goo + quantity: 10 \ No newline at end of file From 76183f6d3ffa72136d157793a83a40cb580f2061 Mon Sep 17 00:00:00 2001 From: Marco Hauptmann Date: Thu, 18 Jan 2024 15:42:56 +0100 Subject: [PATCH 05/20] feat: js --- contracts/src/example-plugins/MOBA/MOBA.js | 734 +++++++++------------ 1 file changed, 312 insertions(+), 422 deletions(-) diff --git a/contracts/src/example-plugins/MOBA/MOBA.js b/contracts/src/example-plugins/MOBA/MOBA.js index 72f3a25aa..39b9d64f2 100644 --- a/contracts/src/example-plugins/MOBA/MOBA.js +++ b/contracts/src/example-plugins/MOBA/MOBA.js @@ -1,60 +1,33 @@ import ds from "downstream"; -const prizeFee = 2; -const prizeItemId = "0x6a7a67f063976de500000001000000010000000000000000"; // green goo -const buildingPrizeBagSlot = 0; -const buildingPrizeItemSlot = 0; const nullBytes24 = `0x${"00".repeat(24)}`; -const duckBuildingTopId = "04"; -const burgerBuildingTopId = "17"; -const burgerCounterKindId = "Burger Display Building"; -const duckCounterKindId = "Duck Display Building"; +const redBuildingTopId = "04"; +const blueBuildingTopId = "17"; +const blueCounterKindId = "🔵 Blue Display Building"; +const redCounterKindId = "🔴 Red Display Building"; const countdownBuildingKindId = "Countdown Building"; -let burgerCounter; -let duckCounter; +let blueCounter; +let redCounter; let countdownBuilding; -let startTime; -let endTime; export default async function update(state) { - // // Action handler functions // - + // An action can set a form submit handler which will be called after the action along with the form values let handleFormSubmit; const join = () => { - if (unitFeeBagSlot < 0) { - console.log( - "fee not found in bags - button should have been disabled", - ); - } const mobileUnit = getMobileUnit(state); const payload = ds.encodeCall("function join()", []); - const dummyBagIdIncaseToBagDoesNotExist = `0x${"00".repeat(24)}`; - - ds.dispatch( - { - name: "TRANSFER_ITEM_MOBILE_UNIT", - args: [ - mobileUnit.id, - [mobileUnit.id, selectedBuilding.id], - [unitFeeBagSlot, buildingPrizeBagSlot], - [unitFeeItemSlot, buildingPrizeItemSlot], - dummyBagIdIncaseToBagDoesNotExist, - prizeFee, - ], - }, - { - name: "BUILDING_USE", - args: [selectedBuilding.id, mobileUnit.id, payload], - }, - ); + 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 @@ -63,24 +36,22 @@ export default async function update(state) { }; const startSubmit = (values) => { - const selectedBuildingIdDuck = values["buildingKindIdDuck"]; - const selectedBuildingIdBurger = values["buildingKindIdBurger"]; - - console.log("start(): form.currentValues", values); + const selectedBuildingKindBaseRed = values["buildingKindIdBaseRed"]; + const selectedBuildTypeBaseBlue = values["buildingKindIdBaseBlue"]; // Verify selected buildings are different from each other - if (selectedBuildingIdDuck == selectedBuildingIdBurger) { + if (selectedBuildingKindBaseRed == selectedBuildTypeBaseBlue) { console.error("Team buildings must be different from each other", { - selectedBuildingIdDuck, - selectedBuildingIdBurger, + selectedBuildingKindBaseRed, + selectedBuildTypeBaseBlue, }); return; } const mobileUnit = getMobileUnit(state); const payload = ds.encodeCall( - "function start(bytes24 duckBuildingID, bytes24 burgerBuildingID)", - [selectedBuildingIdDuck, selectedBuildingIdBurger], + "function start(bytes24 redBaseID, bytes24 blueBaseID)", + [selectedBuildingKindBaseRed, selectedBuildTypeBaseBlue] ); ds.dispatch({ @@ -121,13 +92,13 @@ export default async function update(state) { // 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 = "Duck Burger HQ"; + const dvbBuildingName = "Registery Office"; const selectedBuilding = state.world?.buildings.find( - (b) => b.kind?.name?.value == dvbBuildingName, + (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 ) { + if (!selectedBuilding || !state?.world?.buildings) { console.log("NO DVB BUILDING FOUND"); return { version: 1, @@ -154,30 +125,17 @@ export default async function update(state) { gameActive, startBlock, endBlock, - buildingKindIdDuck, - buildingKindIdBurger, - teamDuckLength, - teamBurgerLength, + buildingKindIdRed, + buildingKindIdBlue, + teamRedLength, + teamBlueLength, } = getHQData(selectedBuilding); - const { unitFeeBagSlot, unitFeeItemSlot } = getMobileUnitFeeSlot(state); - const hasFee = unitFeeBagSlot >= 0; const localBuildings = range5(state, selectedBuilding); - const duckCount = countBuildings( - localBuildings, - buildingKindIdDuck, - startBlock, - endBlock, - ); - const burgerCount = countBuildings( - localBuildings, - buildingKindIdBurger, - startBlock, - endBlock, - ); + const redCount = countBuildings(localBuildings, buildingKindIdRed); + const blueCount = countBuildings(localBuildings, buildingKindIdBlue); - connectDisplayBuildings(state, localBuildings); - + connectDisplayBuildings(state, localBuildings); // check current game state: // - NotStarted : GameActive == false @@ -188,41 +146,44 @@ export default async function update(state) { let buttonList = []; // we build an html block which is rendered above the buttons - let htmlBlock = "

Ducks vs Burgers HQ

"; - htmlBlock += `

payout for win: ${prizeFee * 2}

`; - htmlBlock += `

payout for draw: ${prizeFee}


`; - + let htmlBlock = + '

Red vs Blue

'; + + const canJoin = !gameActive; - const canJoin = !gameActive && hasFee; - const canStart = !gameActive && teamDuckLength > 0 && teamBurgerLength > 0; + const canStart = !gameActive && teamRedLength > 0 && teamBlueLength > 0; if (canJoin) { - htmlBlock += `

total players: ${teamDuckLength + teamBurgerLength}


`; + htmlBlock += `

total players: ${ + teamRedLength + teamBlueLength + }


`; } // Show what team the unit is on const mobileUnit = getMobileUnit(state); let isOnTeam = false; - if (mobileUnit){ - let unitTeam = ''; + if (mobileUnit) { + let unitTeam = ""; - for (let i = 0; i < teamDuckLength; i++) { - if (mobileUnit.id == getHQTeamUnit(selectedBuilding, "Duck", i)) { - unitTeam = '🐤'; + for (let i = 0; i < teamRedLength; i++) { + if (mobileUnit.id == getHQTeamUnit(selectedBuilding, "Red", i)) { + unitTeam = "🐤"; break; } } - if (unitTeam === '') { - for (let i = 0; i < teamBurgerLength; i++) { - if (mobileUnit.id == getHQTeamUnit(selectedBuilding, "Burger", i)) { - unitTeam = '🍔'; + if (unitTeam === "") { + for (let i = 0; i < teamBlueLength; i++) { + if ( + mobileUnit.id == getHQTeamUnit(selectedBuilding, "Blue", i) + ) { + unitTeam = "🍔"; break; } } } - if (unitTeam !== '') { + if (unitTeam !== "") { isOnTeam = true; htmlBlock += `

You are on team ${unitTeam}


@@ -230,31 +191,35 @@ export default async function update(state) { } } - if (!gameActive){ - if (!isOnTeam){ + if (!gameActive) { + if (!isOnTeam) { buttonList.push({ - text: `Join Game (${prizeFee} Green Goo)`, + text: `Join Game`, type: "action", action: join, disabled: !canJoin || isOnTeam, }); - }else{ + } else { // Check reason why game can't start - const waitingForStartCondition = teamDuckLength != teamBurgerLength || teamDuckLength + teamBurgerLength < 2; + const waitingForStartCondition = + teamRedLength != teamBlueLength || + teamRedLength + teamBlueLength < 2; let startConditionMessage = ""; - if (waitingForStartCondition){ - if (teamDuckLength + teamBurgerLength < 2){ - startConditionMessage = "Waiting for players..." - } else if (teamDuckLength != teamBurgerLength){ + if (waitingForStartCondition) { + if (teamRedLength + teamBlueLength < 2) { + startConditionMessage = "Waiting for players..."; + } else if (teamRedLength != teamBlueLength) { startConditionMessage = "Teams must be balanced..."; } } buttonList.push({ - text: waitingForStartCondition ? startConditionMessage : "Start", + text: waitingForStartCondition + ? startConditionMessage + : "Start", type: "action", action: start, - disabled: !canStart || teamDuckLength != teamBurgerLength, + disabled: !canStart || teamRedLength != teamBlueLength, }); } } @@ -263,411 +228,336 @@ export default async function update(state) { // Show options to select team buildings htmlBlock += `

Select Team Buildings

-

Team 🐤

+

🔴 Team 🔴

${getBuildingKindSelectHtml( state, - duckBuildingTopId, - "buildingKindIdDuck", + redBuildingTopId, + "buildingKindIdRed" )} -

Team 🍔

+

🔵 Team 🔵

${getBuildingKindSelectHtml( state, - burgerBuildingTopId, - "buildingKindIdBurger", + blueBuildingTopId, + "buildingKindIdBlue" )} `; } - const nowBlock = state?.world?.block; - const blocksLeft = endBlock > nowBlock ? endBlock - nowBlock : 0; - const blocksFromStart = startBlock < nowBlock ? nowBlock - startBlock : 30; - const timeLeftMs = blocksLeft * 2 * 1000; - const timeSinceStartMs = blocksFromStart * 2 * 1000; - if (gameActive) { // Display selected team buildings const buildingKindDuck = - state.world.buildingKinds.find( - (b) => b.id === buildingKindIdDuck, - ) || {}; + state.world.buildingKinds.find((b) => b.id === buildingKindIdRed) || + {}; const buildingKindBurger = state.world.buildingKinds.find( - (b) => b.id === buildingKindIdBurger, + (b) => b.id === buildingKindIdBlue ) || {}; htmlBlock += `

Team Buildings:

-

Team 🐤: ${buildingKindDuck.name?.value}

-

Team 🍔: ${buildingKindBurger.name?.value}


+

Team 🔴: ${buildingKindDuck.name?.value}

+

Team 🔵: ${buildingKindBurger.name?.value}


`; - if (blocksLeft > 0) { - const now = Date.now(); - if (!startTime) startTime = now - timeSinceStartMs; - if (!endTime) endTime = now + timeLeftMs; - htmlBlock += `

time remaining: ${formatTime(timeLeftMs)}

`; - } else { - // End of game - buttonList.push({ - text: prizePool > 0 ? `Claim Reward` : "Nothing to Claim", - type: "action", - action: claim, - disabled: prizePool == 0, - }); - - htmlBlock += ` -

Game Over:

-

Final Score: 🐤${duckCount} : 🍔${burgerCount} - `; - if (duckCount == burgerCount) { - htmlBlock += ` -

The result was a draw

- `; - } else { - const winningTeamName = - duckCount > burgerCount ? "duck" : "burger"; - const winningTeamEmoji = duckCount > burgerCount ? "🐤" : "🍔"; - htmlBlock += ` -

Team ${winningTeamName} have won the match!

-

${winningTeamEmoji}🏆

- `; - } - } - } else { - startTime = undefined; - endTime = undefined; - } + buttonList.push({ + text: "End Game", + type: "action", + action: claim, + disabled: blueCount === redCount, + }); + + // Reset is always offered (requires some trust!) + buttonList.push({ + text: "Reset", + type: "action", + action: reset, + disabled: false, + }); - // Reset is always offered (requires some trust!) - buttonList.push({ - text: "Reset", - type: "action", - action: reset, - disabled: false, - }); - - // build up an array o fmap objects which are used to update display buildings - // always show the current team counts - const mapObj = [ - { - type: "building", - id: `${burgerCounter ? burgerCounter.id : ""}`, - key: "labelText", - value: `${burgerCount}`, - }, - { - type: "building", - id: `${duckCounter ? duckCounter.id : ""}`, - key: "labelText", - value: `${duckCount}`, - }, - ]; - - // if the game is running show the time - if (gameActive && blocksLeft > 0) { - mapObj.push( + // build up an array o fmap objects which are used to update display buildings + // always show the current team counts + const mapObj = [ { type: "building", - id: `${countdownBuilding ? countdownBuilding.id : ""}`, - key: "countdown-start", - value: `${startTime}`, + id: `${blueCounter ? blueCounter.id : ""}`, + key: "labelText", + value: `${blueCount}`, }, { type: "building", - id: `${countdownBuilding ? countdownBuilding.id : ""}`, - key: "countdown-end", - value: `${endTime}`, + id: `${redCounter ? redCounter.id : ""}`, + key: "labelText", + value: `${redCount}`, }, - ); - } else { - mapObj.push({ - type: "building", - id: `${countdownBuilding ? countdownBuilding.id : ""}`, - key: "labelText", - value: "", - }); - } + ]; - return { - version: 1, - map: mapObj, - components: [ - { - id: "dbhq", - type: "building", - content: [ - { - id: "default", - type: "inline", - html: htmlBlock, - submit: (values) => { - if (typeof handleFormSubmit == "function") { - handleFormSubmit(values); - } + return { + version: 1, + map: mapObj, + components: [ + { + id: "dbhq", + type: "building", + content: [ + { + id: "default", + type: "inline", + html: htmlBlock, + submit: (values) => { + if (typeof handleFormSubmit == "function") { + handleFormSubmit(values); + } + }, + buttons: buttonList, }, - buttons: buttonList, - }, - ], - }, - ], - }; -} - -// --- Duckbur HQ Specific functions - -function getHQData(selectedBuilding) { - const prizePool = getDataInt(selectedBuilding, "prizePool"); - const gameActive = getDataBool(selectedBuilding, "gameActive"); - const startBlock = getDataInt(selectedBuilding, "startBlock"); - const endBlock = getDataInt(selectedBuilding, "endBlock"); - const buildingKindIdDuck = getDataBytes24( - selectedBuilding, - "buildingKindIdDuck", - ); - const buildingKindIdBurger = getDataBytes24( - selectedBuilding, - "buildingKindIdBurger", - ); - const teamDuckLength = getDataInt(selectedBuilding, "teamDuckLength"); - const teamBurgerLength = getDataInt(selectedBuilding, "teamBurgerLength"); - - return { - prizePool, - gameActive, - startBlock, - endBlock, - startBlock, - buildingKindIdDuck, - buildingKindIdBurger, - teamDuckLength, - teamBurgerLength, - }; -} + ], + }, + ], + }; + } -function getHQTeamUnit(selectedBuilding, team, index){ - return getDataBytes24(selectedBuilding, `team${team}Unit_${index}`); -} + // --- Duckbur HQ Specific functions -// search the buildings list ofr the display buildings we're gpoing to use -// for team counts and coutdown -function connectDisplayBuildings(state, buildings) { - if (!burgerCounter) { - burgerCounter = buildings.find((element) => - getBuildingKindsByTileLocation(state, element, burgerCounterKindId), + function getHQData(selectedBuilding) { + const gameActive = getDataBool(selectedBuilding, "gameActive"); + const startBlock = getDataInt(selectedBuilding, "startBlock"); + const endBlock = getDataInt(selectedBuilding, "endBlock"); + const buildingKindIdRed = getDataBytes24( + selectedBuilding, + "buildingKindIdRed" ); - } - if (!duckCounter) { - duckCounter = buildings.find((element) => - getBuildingKindsByTileLocation(state, element, duckCounterKindId), + const buildingKindIdBlue = getDataBytes24( + selectedBuilding, + "buildingKindIdBlue" ); + const teamRedLength = getDataInt(selectedBuilding, "teamRedLength"); + const teamBlueLength = getDataInt(selectedBuilding, "teamBlueLength"); + + return { + prizePool, + gameActive, + startBlock, + endBlock, + startBlock, + buildingKindIdRed, + buildingKindIdBlue, + teamRedLength, + teamBlueLength, + }; } - if (!countdownBuilding) { - countdownBuilding = buildings.find((element) => - getBuildingKindsByTileLocation( - state, - element, - countdownBuildingKindId, - ), - ); + + function getHQTeamUnit(selectedBuilding, team, index) { + return getDataBytes24(selectedBuilding, `team${team}Unit_${index}`); } -} -function formatTime(timeInMs) { - let seconds = Math.floor(timeInMs / 1000); - let minutes = Math.floor(seconds / 60); - let hours = Math.floor(minutes / 60); + // search the buildings list ofr the display buildings we're gpoing to use + // for team counts and coutdown + function connectDisplayBuildings(state, buildings) { + if (!blueCounter) { + blueCounter = buildings.find((element) => + getBuildingKindsByTileLocation( + state, + element, + blueCounterKindId + ) + ); + } + if (!redCounter) { + redCounter = buildings.find((element) => + getBuildingKindsByTileLocation(state, element, redCounterKindId) + ); + } + if (!countdownBuilding) { + countdownBuilding = buildings.find((element) => + getBuildingKindsByTileLocation( + state, + element, + countdownBuildingKindId + ) + ); + } + } - seconds %= 60; - minutes %= 60; + function formatTime(timeInMs) { + let seconds = Math.floor(timeInMs / 1000); + let minutes = Math.floor(seconds / 60); + let hours = Math.floor(minutes / 60); - // Pad each component to ensure two digits - let formattedHours = String(hours).padStart(2, "0"); - let formattedMinutes = String(minutes).padStart(2, "0"); - let formattedSeconds = String(seconds).padStart(2, "0"); + seconds %= 60; + minutes %= 60; - return `${formattedHours}:${formattedMinutes}:${formattedSeconds}`; -} + // Pad each component to ensure two digits + let formattedHours = String(hours).padStart(2, "0"); + let formattedMinutes = String(minutes).padStart(2, "0"); + let formattedSeconds = String(seconds).padStart(2, "0"); -const countBuildings = (buildingsArray, kindID, startBlock, endBlock) => { - return buildingsArray.filter( - (b) => - b.kind?.id == kindID && - b.constructionBlockNum.value >= startBlock && - b.constructionBlockNum.value <= endBlock, - ).length; -}; + return `${formattedHours}:${formattedMinutes}:${formattedSeconds}`; + } -function getMobileUnitFeeSlot(state) { - const mobileUnit = getMobileUnit(state); - const mobileUnitBags = mobileUnit ? getEquipeeBags(state, mobileUnit) : []; - const { bag, slotKey } = findBagAndSlot( - mobileUnitBags, - prizeItemId, - prizeFee, - ); - const unitFeeBagSlot = bag ? bag.equipee.key : -1; - const unitFeeItemSlot = bag ? slotKey : -1; - return { - unitFeeBagSlot, - unitFeeItemSlot, + const countBuildings = (buildingsArray, kindID) => { + return buildingsArray.filter((b) => b.kind?.id == kindID).length; }; -} -function getBuildingKindSelectHtml(state, buildingTopId, selectId) { - return ` + function getBuildingKindSelectHtml(state, buildingTopId, selectId) { + return ` `; -} + } -// --- Generic State helper functions + // --- Generic State helper functions -function getMobileUnit(state) { - return state?.selected?.mobileUnit; -} + 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, - ) - : []; -} + // 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); -} + 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++; - } - }); + // 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; } - return foundBuildings; -} -function hexToSignedDecimal(hex) { - if (hex.startsWith("0x")) { - hex = hex.substr(2); - } + 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); + 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; - } + // Check if the highest bit is set (negative number) + if (num >= maxVal / 2) { + num -= maxVal; + } - return num; -} + return num; + } -function getTileCoords(coords) { - return [ - hexToSignedDecimal(coords[1]), - hexToSignedDecimal(coords[2]), - hexToSignedDecimal(coords[3]), - ]; -} + 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]), - ); -} + 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]) + ); + } -function getBuildingKindsByTileLocation(state, building, kindID) { - return (state?.world?.buildings || []).find( - (b) => b.id === building.id && b.kind?.name?.value == kindID, - ); -} + function getBuildingKindsByTileLocation(state, building, kindID) { + return (state?.world?.buildings || []).find( + (b) => b.id === building.id && b.kind?.name?.value == kindID + ); + } -// get first slot in bags that matches item requirements -function findBagAndSlot(bags, requiredItemId, requiredBalance) { - for (const bag of bags) { - for (const slotKey in bag.slots) { - const slot = bag.slots[slotKey]; - if ( - (!requiredItemId || slot.item.id == requiredItemId) && - requiredBalance <= slot.balance - ) { - return { - bag: bag, - slotKey: slot.key, // assuming each slot has a 'key' property - }; + // get first slot in bags that matches item requirements + function findBagAndSlot(bags, requiredItemId, requiredBalance) { + for (const bag of bags) { + for (const slotKey in bag.slots) { + const slot = bag.slots[slotKey]; + if ( + (!requiredItemId || slot.item.id == requiredItemId) && + requiredBalance <= slot.balance + ) { + return { + bag: bag, + slotKey: slot.key, // assuming each slot has a 'key' property + }; + } } } + return { bag: null, slotKey: -1 }; } - return { bag: null, slotKey: -1 }; -} -// -- Building Data + // -- Building Data -function getData(buildingInstance, key) { - return getKVPs(buildingInstance)[key]; -} + 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 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 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 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; - }, {}); + function getKVPs(buildingInstance) { + return buildingInstance.allData.reduce((kvps, data) => { + kvps[data.name] = data.value; + return kvps; + }, {}); + } } // the source for this code is on github where you can find other example buildings: From 0bee9953f5056c657d0981114779df856d6fa465 Mon Sep 17 00:00:00 2001 From: MH Date: Thu, 18 Jan 2024 15:55:15 +0100 Subject: [PATCH 06/20] Apply suggestions from code review Co-authored-by: billy rennekamp --- contracts/src/example-plugins/MOBA/MOBA.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/src/example-plugins/MOBA/MOBA.js b/contracts/src/example-plugins/MOBA/MOBA.js index 39b9d64f2..8dece6a27 100644 --- a/contracts/src/example-plugins/MOBA/MOBA.js +++ b/contracts/src/example-plugins/MOBA/MOBA.js @@ -36,8 +36,8 @@ export default async function update(state) { }; const startSubmit = (values) => { - const selectedBuildingKindBaseRed = values["buildingKindIdBaseRed"]; - const selectedBuildTypeBaseBlue = values["buildingKindIdBaseBlue"]; + const selectedBuildingKindBaseRed = values["buildingKindIdRed"]; + const selectedBuildTypeBaseBlue = values["buildingKindIdBlue"]; // Verify selected buildings are different from each other if (selectedBuildingKindBaseRed == selectedBuildTypeBaseBlue) { From ee308fd167bc4835363911c9aedb2074800973f7 Mon Sep 17 00:00:00 2001 From: Marco Hauptmann Date: Thu, 18 Jan 2024 16:16:20 +0100 Subject: [PATCH 07/20] stuff --- contracts/src/example-plugins/MOBA/MOBA.js | 32 ++++++++++++---------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/contracts/src/example-plugins/MOBA/MOBA.js b/contracts/src/example-plugins/MOBA/MOBA.js index 8dece6a27..d26f8bb56 100644 --- a/contracts/src/example-plugins/MOBA/MOBA.js +++ b/contracts/src/example-plugins/MOBA/MOBA.js @@ -123,8 +123,6 @@ export default async function update(state) { const { prizePool, gameActive, - startBlock, - endBlock, buildingKindIdRed, buildingKindIdBlue, teamRedLength, @@ -151,7 +149,12 @@ export default async function update(state) { const canJoin = !gameActive; - const canStart = !gameActive && teamRedLength > 0 && teamBlueLength > 0; + const canStart = + !gameActive && + teamRedLength > 0 && + teamBlueLength > 0 && + redCount === 1 && + blueCount === 1; if (canJoin) { htmlBlock += `

total players: ${ @@ -245,26 +248,27 @@ export default async function update(state) { if (gameActive) { // Display selected team buildings - const buildingKindDuck = + const buildingKindRed = state.world.buildingKinds.find((b) => b.id === buildingKindIdRed) || {}; - const buildingKindBurger = + const buildingKindBlue = state.world.buildingKinds.find( (b) => b.id === buildingKindIdBlue ) || {}; htmlBlock += `

Team Buildings:

-

Team 🔴: ${buildingKindDuck.name?.value}

-

Team 🔵: ${buildingKindBurger.name?.value}


+

Team 🔴: ${buildingKindRed.name?.value}

+

Team 🔵: ${buildingKindBlue.name?.value}


`; - buttonList.push({ - text: "End Game", - type: "action", - action: claim, - disabled: blueCount === redCount, - }); + 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({ @@ -316,8 +320,6 @@ export default async function update(state) { }; } - // --- Duckbur HQ Specific functions - function getHQData(selectedBuilding) { const gameActive = getDataBool(selectedBuilding, "gameActive"); const startBlock = getDataInt(selectedBuilding, "startBlock"); From f3fa597dbee6f1a81ce0d98f52c55c4cadc807ee Mon Sep 17 00:00:00 2001 From: Marco Hauptmann Date: Thu, 18 Jan 2024 16:24:05 +0100 Subject: [PATCH 08/20] billy --- contracts/src/example-plugins/MOBA/MOBA.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/src/example-plugins/MOBA/MOBA.js b/contracts/src/example-plugins/MOBA/MOBA.js index d26f8bb56..f4a8182cf 100644 --- a/contracts/src/example-plugins/MOBA/MOBA.js +++ b/contracts/src/example-plugins/MOBA/MOBA.js @@ -92,7 +92,7 @@ export default async function update(state) { // 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 = "Registery Office"; + const dvbBuildingName = "MOBA"; const selectedBuilding = state.world?.buildings.find( (b) => b.kind?.name?.value == dvbBuildingName ); @@ -170,7 +170,7 @@ export default async function update(state) { for (let i = 0; i < teamRedLength; i++) { if (mobileUnit.id == getHQTeamUnit(selectedBuilding, "Red", i)) { - unitTeam = "🐤"; + unitTeam = "🔴"; break; } } @@ -180,7 +180,7 @@ export default async function update(state) { if ( mobileUnit.id == getHQTeamUnit(selectedBuilding, "Blue", i) ) { - unitTeam = "🍔"; + unitTeam = "🔵"; break; } } From 688e53bad6d2c2f7172f1cdafb47996fa47cbd97 Mon Sep 17 00:00:00 2001 From: Marco Hauptmann Date: Thu, 18 Jan 2024 16:25:06 +0100 Subject: [PATCH 09/20] fix --- contracts/src/example-plugins/MOBA/MOBA.js | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/contracts/src/example-plugins/MOBA/MOBA.js b/contracts/src/example-plugins/MOBA/MOBA.js index f4a8182cf..dc1c27f84 100644 --- a/contracts/src/example-plugins/MOBA/MOBA.js +++ b/contracts/src/example-plugins/MOBA/MOBA.js @@ -278,26 +278,8 @@ export default async function update(state) { disabled: false, }); - // build up an array o fmap objects which are used to update display buildings - // always show the current team counts - const mapObj = [ - { - type: "building", - id: `${blueCounter ? blueCounter.id : ""}`, - key: "labelText", - value: `${blueCount}`, - }, - { - type: "building", - id: `${redCounter ? redCounter.id : ""}`, - key: "labelText", - value: `${redCount}`, - }, - ]; - return { version: 1, - map: mapObj, components: [ { id: "dbhq", From fc38388aa1dc74c23d113a227f0647eca12d236c Mon Sep 17 00:00:00 2001 From: Billy Rennekamp Date: Thu, 18 Jan 2024 16:34:47 +0100 Subject: [PATCH 10/20] clean up solidity --- contracts/src/example-plugins/MOBA/MOBA.sol | 310 +------------------- 1 file changed, 8 insertions(+), 302 deletions(-) diff --git a/contracts/src/example-plugins/MOBA/MOBA.sol b/contracts/src/example-plugins/MOBA/MOBA.sol index 8869556e1..186991832 100644 --- a/contracts/src/example-plugins/MOBA/MOBA.sol +++ b/contracts/src/example-plugins/MOBA/MOBA.sol @@ -12,29 +12,15 @@ import "@ds/utils/LibString.sol"; using Schema for State; contract MOBA is BuildingKind { - // MOBA TODO: instead of counting the number of buildings, check whether BaseA and BaseB exist in the game - - // todo - storing contract members like this is per BuildingKind - // to work with building instances and therefore allow multiple buildings - // this data should be stored either as a map to buildingInstance - // or only use SET_DATA_ON_BUILDING action bytes24[] private redTeam; bytes24[] private bleTeam; - // consts - // prize bag info - uint8 constant prizeBagSlot = 0; - uint8 constant prizeItemSlot = 0; - uint64 constant joinFee = 2; - // 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 claim() external {} - function reset() external {} function use( @@ -54,22 +40,9 @@ contract MOBA is BuildingKind { (bytes24, bytes24) ); _start(ds, state, buildingInstance, redBaseID, blueBaseId); - } else if ((bytes4)(payload) == this.claim.selector) { - _claim(ds, state, actor, buildingInstance); } else if ((bytes4)(payload) == this.reset.selector) { _reset(ds, buildingInstance); } - - // ds.getDispatcher().dispatch( - // abi.encodeCall( - // Actions.SET_DATA_ON_BUILDING, - // ( - // buildingInstance, - // "prizePool", - // bytes32(uint256(_calculatePool())) - // ) - // ) - // ); } function _join( @@ -84,32 +57,6 @@ contract MOBA is BuildingKind { revert("Can't join while a game is already active"); } - // TODO: remove prize stuff - - // // 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 currentPrizeBalance = _getPrizeBalance(state, buildingId); - // if ((currentPrizeBalance - joinFee) < lastKnownPrizeBalance) { - // revert("Fee not paid"); - // } - - // // remember the new balance - // Dispatcher dispatcher = ds.getDispatcher(); - // dispatcher.dispatch( - // abi.encodeCall( - // Actions.SET_DATA_ON_BUILDING, - // ( - // buildingId, - // "lastKnownPrizeBalance", - // bytes32(uint256(currentPrizeBalance)) - // ) - // ) - // ); - for (uint256 i = 0; i < redTeam.length; i++) { if (redTeam[i] == unitId) revert("Already joined"); } @@ -216,36 +163,15 @@ contract MOBA is BuildingKind { ) ); - // TODO: remove time stuff - - // // 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))) - // ) - // ); - - // // 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)) - // ) - // ) - // ); + { + (uint24 redBuildings, uint24 blueBuildings) = getBuildingCounts( + state, + buildingId + ); - // // set start to now - // dispatcher.dispatch( - // abi.encodeCall( - // Actions.SET_DATA_ON_BUILDING, - // (buildingId, "startBlock", bytes32(uint256(block.number))) - // ) - // ); + require(redBuildings == 1, "Red team needs a base to start"); + require(blueBuildings == 1, "Blue team needs a base to start"); + } // gameActive dispatcher.dispatch( @@ -256,170 +182,6 @@ contract MOBA is BuildingKind { ); } - function _claim( - Game ds, - State state, - bytes24 unitId, - bytes24 buildingId - ) private { - // check game finished - - // TODO: remove time stuff - // { - // uint256 endBlock = uint256(state.getData(buildingId, "endBlock")); - // if (block.number < endBlock) { - // revert("Can't claim, game is running"); - // } - // } - - // check unit in a team - // check unit not already claimed - bool isRedTeamMember = false; - bool isBlueTeamMember = false; - for (uint256 i = 0; i < redTeam.length; i++) { - if (redTeam[i] == unitId) { - isRedTeamMember = true; - break; - } - } - for (uint256 i = 0; i < blueTeam.length; i++) { - if (blueTeam[i] == unitId) { - isBlueTeamMember = true; - break; - } - } - require( - isRedTeamMember || isBlueTeamMember, - "Unit did not play or has already claimed" - ); - - // count buildings for each team - // NOTE: Scoped to avoid stack being too deep - bool isDraw; - { - (uint24 redBuildings, uint24 blueBuildings) = getBuildingCounts( - state, - buildingId - ); - - if (redBuildings > 1) { - revert("Can't be more than one Red Base"); - } - if (blueBuildings > 1) { - revert("Can't be more than one Blue Base"); - } - - if (redBuildings == 1 && blueBuildings == 1) { - revert("Game isn't over"); - } - - // check unit is in winning team - - if (isRedTeamMember && redBuildings < blueBuildings) { - revert("You, red, are not on the winning team: blue"); - } else if (isBlueTeamMember && blueBuildings < redBuildings) { - revert("You, blue, are not on the winning team: red"); - } - } - - // // winner! (or drawer) - // // \todo this currently assumes even teams - // Dispatcher dispatcher = ds.getDispatcher(); - // _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))) - // ) - // ) - // ); - - // Remove unit from team so they can't double claim - if (isRedTeamMember) { - removeUnitFromArray(redTeam, unitId); - } else if (isBlueTeamMember) { - removeUnitFromArray(blueteam, unitId); - } - } - - // 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 - // ); - - // (uint8 destBagSlot, uint8 destItemSlot) = _findValidItemSlot( - // state, - // unitId, - // prizeItemId, - // prizeAmount - // ); - - // dispatcher.dispatch( - // abi.encodeCall( - // Actions.TRANSFER_ITEM_MOBILE_UNIT, - // ( - // buildingId, - // [buildingId, unitId], - // [prizeBagSlot, destBagSlot], - // [prizeItemSlot, destItemSlot], - // bytes24(0), // To bag ID not required - // prizeAmount - // ) - // ) - // ); - // } - - // 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" - // ); - - // for (destItemSlot = 0; destItemSlot < 4; destItemSlot++) { - // (bytes24 destItemId, uint64 destBalance) = state.getItemSlot( - // destBagId, - // destItemSlot - // ); - // if ( - // (destItemId == bytes24(0) || destItemId == itemId) && - // destBalance + transferAmount <= 100 - // ) { - // // Found valid slot - // return (destBagSlot, destItemSlot); - // } - // } - // } - - // revert("No valid slot for prize claim found"); - // } - function removeUnitFromArray( bytes24[] storage array, bytes24 unitId @@ -439,19 +201,6 @@ contract MOBA is BuildingKind { // todo - do we check if all claims have been made ? // for now allwing reset any time which requires some trust :) - // set state to joining (gameActive ?) - // dispatcher.dispatch( - // 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))) - // ) - // ); dispatcher.dispatch( abi.encodeCall( Actions.SET_DATA_ON_BUILDING, @@ -472,18 +221,6 @@ contract MOBA is BuildingKind { (buildingId, "redTeamLength", bytes32(0)) ) ); - // dispatcher.dispatch( - // 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, @@ -498,23 +235,6 @@ contract MOBA is BuildingKind { ); } - // function _getPrizeBalance( - // State state, - // bytes24 buildingId - // ) internal view returns (uint64) { - // bytes24 prizeBag = state.getEquipSlot(buildingId, prizeBagSlot); - // (, uint64 balance) = state.getItemSlot(prizeBag, prizeItemSlot); - // return balance; - // } - - // function _calculatePrizeAmount() internal pure returns (uint64) { - // return joinFee * 2; - // } - - // function _calculatePool() internal view returns (uint64) { - // return uint64(teamDuckUnits.length + teamBurgerUnits.length) * joinFee; - // } - function getBuildingCounts( State state, bytes24 buildingInstance @@ -540,25 +260,11 @@ contract MOBA is BuildingKind { coords(arenaTiles[i])[3] ); if (state.getBuildingKind(arenaBuildingID) == redBuildingKind) { - // uint64 constructionBlockNum = state - // .getBuildingConstructionBlockNum(arenaBuildingID); - // if ( - // constructionBlockNum >= startBlock && - // constructionBlockNum <= endBlock - // ) { reds++; - // } } else if ( state.getBuildingKind(arenaBuildingID) == blueBuildingKind ) { - // uint64 constructionBlockNum = state - // .getBuildingConstructionBlockNum(arenaBuildingID); - // if ( - // constructionBlockNum >= startBlock && - // constructionBlockNum <= endBlock - // ) { blues++; - // } } } } From 9c9fc6407e60e782f2c8ed0cfa51ab1b2efcc73b Mon Sep 17 00:00:00 2001 From: Marco Hauptmann Date: Thu, 18 Jan 2024 16:41:41 +0100 Subject: [PATCH 11/20] stuff --- contracts/src/example-plugins/MOBA/MOBA.js | 75 ---------------------- 1 file changed, 75 deletions(-) diff --git a/contracts/src/example-plugins/MOBA/MOBA.js b/contracts/src/example-plugins/MOBA/MOBA.js index dc1c27f84..c1e5cd0a0 100644 --- a/contracts/src/example-plugins/MOBA/MOBA.js +++ b/contracts/src/example-plugins/MOBA/MOBA.js @@ -3,13 +3,6 @@ import ds from "downstream"; const nullBytes24 = `0x${"00".repeat(24)}`; const redBuildingTopId = "04"; const blueBuildingTopId = "17"; -const blueCounterKindId = "🔵 Blue Display Building"; -const redCounterKindId = "🔴 Red Display Building"; -const countdownBuildingKindId = "Countdown Building"; - -let blueCounter; -let redCounter; -let countdownBuilding; export default async function update(state) { // @@ -133,8 +126,6 @@ export default async function update(state) { const redCount = countBuildings(localBuildings, buildingKindIdRed); const blueCount = countBuildings(localBuildings, buildingKindIdBlue); - connectDisplayBuildings(state, localBuildings); - // check current game state: // - NotStarted : GameActive == false // - Running : GameActive == true && endBlock < currentBlock @@ -336,47 +327,6 @@ export default async function update(state) { // search the buildings list ofr the display buildings we're gpoing to use // for team counts and coutdown - function connectDisplayBuildings(state, buildings) { - if (!blueCounter) { - blueCounter = buildings.find((element) => - getBuildingKindsByTileLocation( - state, - element, - blueCounterKindId - ) - ); - } - if (!redCounter) { - redCounter = buildings.find((element) => - getBuildingKindsByTileLocation(state, element, redCounterKindId) - ); - } - if (!countdownBuilding) { - countdownBuilding = buildings.find((element) => - getBuildingKindsByTileLocation( - state, - element, - countdownBuildingKindId - ) - ); - } - } - - function formatTime(timeInMs) { - let seconds = Math.floor(timeInMs / 1000); - let minutes = Math.floor(seconds / 60); - let hours = Math.floor(minutes / 60); - - seconds %= 60; - minutes %= 60; - - // Pad each component to ensure two digits - let formattedHours = String(hours).padStart(2, "0"); - let formattedMinutes = String(minutes).padStart(2, "0"); - let formattedSeconds = String(seconds).padStart(2, "0"); - - return `${formattedHours}:${formattedMinutes}:${formattedSeconds}`; - } const countBuildings = (buildingsArray, kindID) => { return buildingsArray.filter((b) => b.kind?.id == kindID).length; @@ -490,31 +440,6 @@ export default async function update(state) { ); } - function getBuildingKindsByTileLocation(state, building, kindID) { - return (state?.world?.buildings || []).find( - (b) => b.id === building.id && b.kind?.name?.value == kindID - ); - } - - // get first slot in bags that matches item requirements - function findBagAndSlot(bags, requiredItemId, requiredBalance) { - for (const bag of bags) { - for (const slotKey in bag.slots) { - const slot = bag.slots[slotKey]; - if ( - (!requiredItemId || slot.item.id == requiredItemId) && - requiredBalance <= slot.balance - ) { - return { - bag: bag, - slotKey: slot.key, // assuming each slot has a 'key' property - }; - } - } - } - return { bag: null, slotKey: -1 }; - } - // -- Building Data function getData(buildingInstance, key) { From 676fb307382ac44f4514f761d1f93c3e07a47320 Mon Sep 17 00:00:00 2001 From: MH Date: Thu, 18 Jan 2024 16:43:22 +0100 Subject: [PATCH 12/20] Apply suggestions from code review Co-authored-by: billy rennekamp --- contracts/src/example-plugins/MOBA/MOBA.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/src/example-plugins/MOBA/MOBA.js b/contracts/src/example-plugins/MOBA/MOBA.js index c1e5cd0a0..108317e7d 100644 --- a/contracts/src/example-plugins/MOBA/MOBA.js +++ b/contracts/src/example-plugins/MOBA/MOBA.js @@ -296,7 +296,7 @@ export default async function update(state) { function getHQData(selectedBuilding) { const gameActive = getDataBool(selectedBuilding, "gameActive"); const startBlock = getDataInt(selectedBuilding, "startBlock"); - const endBlock = getDataInt(selectedBuilding, "endBlock"); + // const endBlock = getDataInt(selectedBuilding, "endBlock"); const buildingKindIdRed = getDataBytes24( selectedBuilding, "buildingKindIdRed" From 2eaa86d782588aa30ba83033a4534427616a61cd Mon Sep 17 00:00:00 2001 From: MH Date: Thu, 18 Jan 2024 16:43:36 +0100 Subject: [PATCH 13/20] Update contracts/src/example-plugins/MOBA/MOBA.js Co-authored-by: billy rennekamp --- contracts/src/example-plugins/MOBA/MOBA.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/src/example-plugins/MOBA/MOBA.js b/contracts/src/example-plugins/MOBA/MOBA.js index 108317e7d..3b47e1594 100644 --- a/contracts/src/example-plugins/MOBA/MOBA.js +++ b/contracts/src/example-plugins/MOBA/MOBA.js @@ -295,7 +295,7 @@ export default async function update(state) { function getHQData(selectedBuilding) { const gameActive = getDataBool(selectedBuilding, "gameActive"); - const startBlock = getDataInt(selectedBuilding, "startBlock"); + // const startBlock = getDataInt(selectedBuilding, "startBlock"); // const endBlock = getDataInt(selectedBuilding, "endBlock"); const buildingKindIdRed = getDataBytes24( selectedBuilding, From d9bcb187f947c360b9441a7f13e32c194c7cc166 Mon Sep 17 00:00:00 2001 From: Billy Rennekamp Date: Fri, 19 Jan 2024 10:52:12 +0100 Subject: [PATCH 14/20] fix deploy error --- .../DuckBurger/DuckBurgerHQ.sol | 363 +++++++-- contracts/src/example-plugins/MOBA/MOBA.sol | 13 +- contracts/src/schema/Schema.sol | 736 ++++++++++++++---- 3 files changed, 881 insertions(+), 231 deletions(-) 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.sol b/contracts/src/example-plugins/MOBA/MOBA.sol index 186991832..9ca8a1c95 100644 --- a/contracts/src/example-plugins/MOBA/MOBA.sol +++ b/contracts/src/example-plugins/MOBA/MOBA.sol @@ -13,7 +13,7 @@ using Schema for State; contract MOBA is BuildingKind { bytes24[] private redTeam; - bytes24[] private bleTeam; + bytes24[] private blueTeam; // function declerations only used to create signatures for the use payload // these functions do not have their own definitions @@ -53,6 +53,10 @@ contract MOBA is BuildingKind { ) 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"); } @@ -208,7 +212,7 @@ contract MOBA is BuildingKind { ) ); delete redTeam; - delete blueteam; + delete blueTeam; dispatcher.dispatch( abi.encodeCall( Actions.SET_DATA_ON_BUILDING, @@ -245,11 +249,6 @@ contract MOBA is BuildingKind { bytes24 blueBuildingKind = bytes24( state.getData(buildingInstance, "buildingKindIdBlue") ); - // uint256 endBlock = uint256(state.getData(buildingInstance, "endBlock")); - // 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++) { 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)); } } From 10159beefa60ff38cfafe5be36a8894b97098630 Mon Sep 17 00:00:00 2001 From: Billy Rennekamp Date: Fri, 19 Jan 2024 10:54:37 +0100 Subject: [PATCH 15/20] fix js --- contracts/src/example-plugins/MOBA/MOBA.js | 803 ++++++++++----------- 1 file changed, 399 insertions(+), 404 deletions(-) diff --git a/contracts/src/example-plugins/MOBA/MOBA.js b/contracts/src/example-plugins/MOBA/MOBA.js index 3b47e1594..2fc7511f9 100644 --- a/contracts/src/example-plugins/MOBA/MOBA.js +++ b/contracts/src/example-plugins/MOBA/MOBA.js @@ -5,468 +5,463 @@ const redBuildingTopId = "04"; const blueBuildingTopId = "17"; export default async function update(state) { - // - // Action handler functions - // + // + // Action handler functions + // - // An action can set a form submit handler which will be called after the action along with the form values - let handleFormSubmit; + // 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 join = () => { + const mobileUnit = getMobileUnit(state); - const mobileUnit = getMobileUnit(state); - const payload = ds.encodeCall( - "function start(bytes24 redBaseID, bytes24 blueBaseID)", - [selectedBuildingKindBaseRed, selectedBuildTypeBaseBlue] - ); + 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; + } - ds.dispatch({ - name: "BUILDING_USE", - args: [selectedBuilding.id, mobileUnit.id, payload], - }); - }; + const mobileUnit = getMobileUnit(state); + const payload = ds.encodeCall( + "function start(bytes24 redBaseID, bytes24 blueBaseID)", + [selectedBuildingKindBaseRed, selectedBuildTypeBaseBlue] + ); - const claim = () => { - const mobileUnit = getMobileUnit(state); + ds.dispatch({ + name: "BUILDING_USE", + args: [selectedBuilding.id, mobileUnit.id, payload], + }); + }; - const payload = ds.encodeCall("function claim()", []); + const claim = () => { + const mobileUnit = getMobileUnit(state); - ds.dispatch({ - name: "BUILDING_USE", - args: [selectedBuilding.id, mobileUnit.id, payload], - }); - }; + const payload = ds.encodeCall("function claim()", []); - const reset = () => { - const mobileUnit = getMobileUnit(state); - const payload = ds.encodeCall("function reset()", []); + ds.dispatch({ + name: "BUILDING_USE", + args: [selectedBuilding.id, mobileUnit.id, payload], + }); + }; - 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 DVB BUILDING FOUND"); + return { + version: 1, + map: [], + components: [ + { + id: "dbhq", + type: "building", + content: [ + { + id: "default", + type: "inline", + html: "", + buttons: [], + }, + ], + }, + ], }; - - // 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 DVB BUILDING FOUND"); - return { - version: 1, - map: [], - components: [ - { - id: "dbhq", - type: "building", - content: [ - { - id: "default", - type: "inline", - html: "", - buttons: [], - }, - ], - }, - ], - }; + } + + const { + prizePool, + gameActive, + buildingKindIdRed, + buildingKindIdBlue, + teamRedLength, + teamBlueLength, + } = getHQData(selectedBuilding); + + const localBuildings = range5(state, selectedBuilding); + const redCount = countBuildings(localBuildings, buildingKindIdRed); + const blueCount = countBuildings(localBuildings, buildingKindIdBlue); + + // 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 = + '

Red vs Blue

'; + + const canJoin = !gameActive; + + const canStart = + !gameActive && + teamRedLength > 0 && + teamBlueLength > 0 && + redCount === 1 && + blueCount === 1; + + if (canJoin) { + htmlBlock += `

total players: ${teamRedLength + teamBlueLength + }


`; + } + + // Show what team the unit is on + const mobileUnit = getMobileUnit(state); + let isOnTeam = false; + if (mobileUnit) { + let unitTeam = ""; + + for (let i = 0; i < teamRedLength; i++) { + if (mobileUnit.id == getHQTeamUnit(selectedBuilding, "Red", i)) { + unitTeam = "🔴"; + break; + } } - const { - prizePool, - gameActive, - buildingKindIdRed, - buildingKindIdBlue, - teamRedLength, - teamBlueLength, - } = getHQData(selectedBuilding); - - const localBuildings = range5(state, selectedBuilding); - const redCount = countBuildings(localBuildings, buildingKindIdRed); - const blueCount = countBuildings(localBuildings, buildingKindIdBlue); - - // 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 = - '

Red vs Blue

'; - - const canJoin = !gameActive; - - const canStart = - !gameActive && - teamRedLength > 0 && - teamBlueLength > 0 && - redCount === 1 && - blueCount === 1; - - if (canJoin) { - htmlBlock += `

total players: ${ - teamRedLength + teamBlueLength - }


`; - } - - // Show what team the unit is on - const mobileUnit = getMobileUnit(state); - let isOnTeam = false; - if (mobileUnit) { - let unitTeam = ""; - - for (let i = 0; i < teamRedLength; i++) { - if (mobileUnit.id == getHQTeamUnit(selectedBuilding, "Red", i)) { - unitTeam = "🔴"; - break; - } - } - - if (unitTeam === "") { - for (let i = 0; i < teamBlueLength; i++) { - if ( - mobileUnit.id == getHQTeamUnit(selectedBuilding, "Blue", i) - ) { - unitTeam = "🔵"; - break; - } - } + if (unitTeam === "") { + for (let i = 0; i < teamBlueLength; i++) { + if ( + mobileUnit.id == getHQTeamUnit(selectedBuilding, "Blue", i) + ) { + unitTeam = "🔵"; + break; } + } + } - if (unitTeam !== "") { - isOnTeam = true; - htmlBlock += ` + if (unitTeam !== "") { + isOnTeam = true; + htmlBlock += `

You are on team ${unitTeam}


`; - } } - - 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 = - teamRedLength != teamBlueLength || - teamRedLength + teamBlueLength < 2; - let startConditionMessage = ""; - if (waitingForStartCondition) { - if (teamRedLength + teamBlueLength < 2) { - startConditionMessage = "Waiting for players..."; - } else if (teamRedLength != teamBlueLength) { - startConditionMessage = "Teams must be balanced..."; - } - } - - buttonList.push({ - text: waitingForStartCondition - ? startConditionMessage - : "Start", - type: "action", - action: start, - disabled: !canStart || teamRedLength != teamBlueLength, - }); + } + + 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 = + teamRedLength != teamBlueLength || + teamRedLength + teamBlueLength < 2; + let startConditionMessage = ""; + if (waitingForStartCondition) { + if (teamRedLength + teamBlueLength < 2) { + startConditionMessage = "Waiting for players..."; + } else if (teamRedLength != teamBlueLength) { + startConditionMessage = "Teams must be balanced..."; } + } + + buttonList.push({ + text: waitingForStartCondition + ? startConditionMessage + : "Start", + type: "action", + action: start, + disabled: !canStart || teamRedLength != teamBlueLength, + }); } + } - if (canStart) { - // Show options to select team buildings - htmlBlock += ` + if (canStart) { + // Show options to select team buildings + htmlBlock += `

Select Team Buildings

🔴 Team 🔴

${getBuildingKindSelectHtml( - state, - redBuildingTopId, - "buildingKindIdRed" - )} + state, + redBuildingTopId, + "buildingKindIdRed" + )}

🔵 Team 🔵

${getBuildingKindSelectHtml( - state, - blueBuildingTopId, - "buildingKindIdBlue" - )} + 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 += ` + } + + 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 Buildings:

Team 🔴: ${buildingKindRed.name?.value}

Team 🔵: ${buildingKindBlue.name?.value}


`; - if (redCount !== blueCount) { - const redWon = redCount > blueCount; - htmlBlock += ` + 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, - }); - - return { - version: 1, - components: [ - { - id: "dbhq", - 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 teamRedLength = getDataInt(selectedBuilding, "teamRedLength"); - const teamBlueLength = getDataInt(selectedBuilding, "teamBlueLength"); - - return { - prizePool, - gameActive, - startBlock, - endBlock, - startBlock, - buildingKindIdRed, - buildingKindIdBlue, - teamRedLength, - teamBlueLength, - }; - } + // Reset is always offered (requires some trust!) + buttonList.push({ + text: "Reset", + type: "action", + action: reset, + disabled: false, + }); + + return { + version: 1, + components: [ + { + id: "dbhq", + 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 teamRedLength = getDataInt(selectedBuilding, "teamRedLength"); + const teamBlueLength = getDataInt(selectedBuilding, "teamBlueLength"); + + return { + gameActive, + buildingKindIdRed, + buildingKindIdBlue, + teamRedLength, + teamBlueLength, + }; + } - function getHQTeamUnit(selectedBuilding, team, index) { - return getDataBytes24(selectedBuilding, `team${team}Unit_${index}`); - } + function getHQTeamUnit(selectedBuilding, team, index) { + return getDataBytes24(selectedBuilding, `team${team}Unit_${index}`); + } - // search the buildings list ofr the display buildings we're gpoing to use - // for team counts and coutdown + // search the buildings list ofr the display buildings we're gpoing to use + // for team counts and coutdown - const countBuildings = (buildingsArray, kindID) => { - return buildingsArray.filter((b) => b.kind?.id == kindID).length; - }; + const countBuildings = (buildingsArray, kindID) => { + return buildingsArray.filter((b) => b.kind?.id == kindID).length; + }; - function getBuildingKindSelectHtml(state, buildingTopId, selectId) { - return ` + 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++ + } + + // --- 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] ) { - 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++; - } - }); - } + foundBuildings[i] = b; + i++; } + }); } - return foundBuildings; + } } + 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]) - ); + function hexToSignedDecimal(hex) { + if (hex.startsWith("0x")) { + hex = hex.substr(2); } - // -- Building Data + let num = parseInt(hex, 16); + let bits = hex.length * 4; + let maxVal = Math.pow(2, bits); - function getData(buildingInstance, key) { - return getKVPs(buildingInstance)[key]; + // Check if the highest bit is set (negative number) + if (num >= maxVal / 2) { + num -= maxVal; } - 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; - }, {}); - } + 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; + }, {}); + } } // the source for this code is on github where you can find other example buildings: From 7dc3be0646a2bd1454db72ba446cd13dfc760c1c Mon Sep 17 00:00:00 2001 From: Billy Rennekamp Date: Fri, 19 Jan 2024 10:56:57 +0100 Subject: [PATCH 16/20] remove prizepool --- contracts/src/example-plugins/MOBA/MOBA.js | 1 - 1 file changed, 1 deletion(-) diff --git a/contracts/src/example-plugins/MOBA/MOBA.js b/contracts/src/example-plugins/MOBA/MOBA.js index 2fc7511f9..f3400f62a 100644 --- a/contracts/src/example-plugins/MOBA/MOBA.js +++ b/contracts/src/example-plugins/MOBA/MOBA.js @@ -114,7 +114,6 @@ export default async function update(state) { } const { - prizePool, gameActive, buildingKindIdRed, buildingKindIdBlue, From 6d864f5edaeb457170944bfe10aa300d4d6a59d9 Mon Sep 17 00:00:00 2001 From: Billy Rennekamp Date: Fri, 19 Jan 2024 11:03:08 +0100 Subject: [PATCH 17/20] js fix --- contracts/src/example-plugins/MOBA/MOBA.js | 280 +++++++++++---------- 1 file changed, 141 insertions(+), 139 deletions(-) diff --git a/contracts/src/example-plugins/MOBA/MOBA.js b/contracts/src/example-plugins/MOBA/MOBA.js index f3400f62a..2183ca4df 100644 --- a/contracts/src/example-plugins/MOBA/MOBA.js +++ b/contracts/src/example-plugins/MOBA/MOBA.js @@ -9,6 +9,7 @@ export default async function update(state) { // Action handler functions // + // An action can set a form submit handler which will be called after the action along with the form values let handleFormSubmit; @@ -290,178 +291,179 @@ export default async function update(state) { ], }; } +} - 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 teamRedLength = getDataInt(selectedBuilding, "teamRedLength"); - const teamBlueLength = getDataInt(selectedBuilding, "teamBlueLength"); - - return { - gameActive, - buildingKindIdRed, - buildingKindIdBlue, - teamRedLength, - teamBlueLength, - }; - } +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 teamRedLength = getDataInt(selectedBuilding, "teamRedLength"); + const teamBlueLength = getDataInt(selectedBuilding, "teamBlueLength"); - function getHQTeamUnit(selectedBuilding, team, index) { - return getDataBytes24(selectedBuilding, `team${team}Unit_${index}`); - } + return { + gameActive, + buildingKindIdRed, + buildingKindIdBlue, + teamRedLength, + teamBlueLength, + }; +} - // search the buildings list ofr the display buildings we're gpoing to use - // for team counts and coutdown +function getHQTeamUnit(selectedBuilding, team, index) { + return getDataBytes24(selectedBuilding, `team${team}Unit_${index}`); +} - const countBuildings = (buildingsArray, kindID) => { - return buildingsArray.filter((b) => b.kind?.id == kindID).length; - }; +// search the buildings list ofr the display buildings we're gpoing to use +// for team counts and coutdown - function getBuildingKindSelectHtml(state, buildingTopId, selectId) { - return ` +function getBuildingKindSelectHtml(state, buildingTopId, selectId) { + return ` `; - } +} - // --- Generic State helper functions +// --- Generic State helper functions - function getMobileUnit(state) { - return state?.selected?.mobileUnit; - } +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 - ) - : []; - } +// 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); - } +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++; - } - }); - } +// 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; } + 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); +function hexToSignedDecimal(hex) { + if (hex.startsWith("0x")) { + hex = hex.substr(2); + } - // Check if the highest bit is set (negative number) - if (num >= maxVal / 2) { - num -= maxVal; - } + let num = parseInt(hex, 16); + let bits = hex.length * 4; + let maxVal = Math.pow(2, bits); - return num; + // Check if the highest bit is set (negative number) + if (num >= maxVal / 2) { + num -= maxVal; } - function getTileCoords(coords) { - return [ - hexToSignedDecimal(coords[1]), - hexToSignedDecimal(coords[2]), - hexToSignedDecimal(coords[3]), - ]; - } + return num; +} - 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]) - ); - } +function getTileCoords(coords) { + return [ + hexToSignedDecimal(coords[1]), + hexToSignedDecimal(coords[2]), + hexToSignedDecimal(coords[3]), + ]; +} - // -- Building Data +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]) + ); +} - function getData(buildingInstance, key) { - return getKVPs(buildingInstance)[key]; - } +// -- Building Data - function getDataBool(buildingInstance, key) { - var hexVal = getData(buildingInstance, key); - return typeof hexVal === "string" ? parseInt(hexVal, 16) == 1 : false; - } +function getData(buildingInstance, key) { + return getKVPs(buildingInstance)[key]; +} - function getDataInt(buildingInstance, key) { - var hexVal = getData(buildingInstance, key); - return typeof hexVal === "string" ? parseInt(hexVal, 16) : 0; - } +function getDataBool(buildingInstance, key) { + var hexVal = getData(buildingInstance, key); + return typeof hexVal === "string" ? parseInt(hexVal, 16) == 1 : false; +} - function getDataBytes24(buildingInstance, key) { - var hexVal = getData(buildingInstance, key); - return typeof hexVal === "string" ? hexVal.slice(0, -16) : nullBytes24; - } +function getDataInt(buildingInstance, key) { + var hexVal = getData(buildingInstance, key); + return typeof hexVal === "string" ? parseInt(hexVal, 16) : 0; +} - function getKVPs(buildingInstance) { - return buildingInstance.allData.reduce((kvps, data) => { - kvps[data.name] = data.value; - return kvps; - }, {}); - } +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; + }, {}); +} + + // 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 + +const countBuildings = (buildingsArray, kindID) => { + return buildingsArray.filter((b) => b.kind?.id == kindID).length; +}; From 015d9a5de3d038931b8ffd611b7350d28a04c0bb Mon Sep 17 00:00:00 2001 From: Billy Rennekamp Date: Fri, 19 Jan 2024 11:12:43 +0100 Subject: [PATCH 18/20] js fix --- contracts/src/example-plugins/MOBA/MOBA.js | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/contracts/src/example-plugins/MOBA/MOBA.js b/contracts/src/example-plugins/MOBA/MOBA.js index 2183ca4df..d223e11e2 100644 --- a/contracts/src/example-plugins/MOBA/MOBA.js +++ b/contracts/src/example-plugins/MOBA/MOBA.js @@ -93,13 +93,13 @@ export default async function update(state) { // early out if we don't have any buildings or state isn't ready if (!selectedBuilding || !state?.world?.buildings) { - console.log("NO DVB BUILDING FOUND"); + console.log("NO MOBA BUILDING FOUND"); return { version: 1, map: [], components: [ { - id: "dbhq", + id: "moba", type: "building", content: [ { @@ -272,7 +272,8 @@ export default async function update(state) { version: 1, components: [ { - id: "dbhq", + id: "moba", + map: [], type: "building", content: [ { @@ -460,10 +461,10 @@ function getKVPs(buildingInstance) { }, {}); } - -// 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 - 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 From 1e2674c7a01045b33c8bf473ce6c3c5c601af2b8 Mon Sep 17 00:00:00 2001 From: Billy Rennekamp Date: Fri, 19 Jan 2024 11:27:50 +0100 Subject: [PATCH 19/20] js fix --- contracts/src/example-plugins/MOBA/MOBA.js | 75 ++++++++++------------ 1 file changed, 35 insertions(+), 40 deletions(-) diff --git a/contracts/src/example-plugins/MOBA/MOBA.js b/contracts/src/example-plugins/MOBA/MOBA.js index d223e11e2..9f66714e2 100644 --- a/contracts/src/example-plugins/MOBA/MOBA.js +++ b/contracts/src/example-plugins/MOBA/MOBA.js @@ -8,7 +8,7 @@ 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; @@ -54,17 +54,6 @@ export default async function update(state) { }); }; - const claim = () => { - const mobileUnit = getMobileUnit(state); - - const payload = ds.encodeCall("function claim()", []); - - ds.dispatch({ - name: "BUILDING_USE", - args: [selectedBuilding.id, mobileUnit.id, payload], - }); - }; - const reset = () => { const mobileUnit = getMobileUnit(state); const payload = ds.encodeCall("function reset()", []); @@ -91,9 +80,10 @@ export default async function update(state) { (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"); + console.log("NO MOBA BUILDING FOUND 3"); return { version: 1, map: [], @@ -114,6 +104,7 @@ export default async function update(state) { }; } + const { gameActive, buildingKindIdRed, @@ -121,11 +112,9 @@ export default async function update(state) { teamRedLength, teamBlueLength, } = getHQData(selectedBuilding); - const localBuildings = range5(state, selectedBuilding); const redCount = countBuildings(localBuildings, buildingKindIdRed); const blueCount = countBuildings(localBuildings, buildingKindIdBlue); - // check current game state: // - NotStarted : GameActive == false // - Running : GameActive == true && endBlock < currentBlock @@ -146,7 +135,6 @@ export default async function update(state) { teamBlueLength > 0 && redCount === 1 && blueCount === 1; - if (canJoin) { htmlBlock += `

total players: ${teamRedLength + teamBlueLength }


`; @@ -154,6 +142,7 @@ export default async function update(state) { // Show what team the unit is on const mobileUnit = getMobileUnit(state); + let isOnTeam = false; if (mobileUnit) { let unitTeam = ""; @@ -185,7 +174,9 @@ export default async function update(state) { } if (!gameActive) { + if (!isOnTeam) { + buttonList.push({ text: `Join Game`, type: "action", @@ -217,11 +208,13 @@ export default async function update(state) { } } + if (canStart) { + // Show options to select team buildings htmlBlock += `

Select Team Buildings

-

🔴 Team 🔴

+

🔴 Team 🔴

${getBuildingKindSelectHtml( state, redBuildingTopId, @@ -260,6 +253,7 @@ export default async function update(state) { `; } + // Reset is always offered (requires some trust!) buttonList.push({ text: "Reset", @@ -267,31 +261,32 @@ export default async function update(state) { action: reset, disabled: false, }); + } - return { - version: 1, - components: [ - { - id: "moba", - map: [], - type: "building", - content: [ - { - id: "default", - type: "inline", - html: htmlBlock, - submit: (values) => { - if (typeof handleFormSubmit == "function") { - handleFormSubmit(values); - } - }, - buttons: buttonList, + 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) { From 704285305c941ec80a276924489ca1771e681fc4 Mon Sep 17 00:00:00 2001 From: Billy Rennekamp Date: Fri, 19 Jan 2024 12:11:20 +0100 Subject: [PATCH 20/20] js fix --- contracts/src/example-plugins/MOBA/MOBA.js | 97 +++++++++++---------- contracts/src/example-plugins/MOBA/MOBA.sol | 4 + 2 files changed, 55 insertions(+), 46 deletions(-) diff --git a/contracts/src/example-plugins/MOBA/MOBA.js b/contracts/src/example-plugins/MOBA/MOBA.js index 9f66714e2..9e257318e 100644 --- a/contracts/src/example-plugins/MOBA/MOBA.js +++ b/contracts/src/example-plugins/MOBA/MOBA.js @@ -1,8 +1,8 @@ import ds from "downstream"; const nullBytes24 = `0x${"00".repeat(24)}`; -const redBuildingTopId = "04"; -const blueBuildingTopId = "17"; +const redBuildingTopId = "14"; +const blueBuildingTopId = "14"; export default async function update(state) { // @@ -83,7 +83,7 @@ export default async function update(state) { // 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 3"); + console.log("NO MOBA BUILDING FOUND 8"); return { version: 1, map: [], @@ -109,12 +109,20 @@ export default async function update(state) { gameActive, buildingKindIdRed, buildingKindIdBlue, - teamRedLength, - teamBlueLength, + 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 @@ -131,13 +139,12 @@ export default async function update(state) { const canStart = !gameActive && - teamRedLength > 0 && - teamBlueLength > 0 && - redCount === 1 && - blueCount === 1; + redTeamLength > 0 && + blueTeamLength > 0 + + console.log({ canStart, canJoin }) if (canJoin) { - htmlBlock += `

total players: ${teamRedLength + teamBlueLength - }


`; + htmlBlock += `

total players: ${redTeamLength + blueTeamLength}


`; } // Show what team the unit is on @@ -147,17 +154,17 @@ export default async function update(state) { if (mobileUnit) { let unitTeam = ""; - for (let i = 0; i < teamRedLength; i++) { - if (mobileUnit.id == getHQTeamUnit(selectedBuilding, "Red", i)) { + for (let i = 0; i < redTeamLength; i++) { + if (mobileUnit.id == getHQTeamUnit(selectedBuilding, "red", i)) { unitTeam = "🔴"; break; } } if (unitTeam === "") { - for (let i = 0; i < teamBlueLength; i++) { + for (let i = 0; i < blueTeamLength; i++) { if ( - mobileUnit.id == getHQTeamUnit(selectedBuilding, "Blue", i) + mobileUnit.id == getHQTeamUnit(selectedBuilding, "blue", i) ) { unitTeam = "🔵"; break; @@ -173,6 +180,8 @@ export default async function update(state) { } } + console.log({ isOnTeam }) + if (!gameActive) { if (!isOnTeam) { @@ -186,13 +195,13 @@ export default async function update(state) { } else { // Check reason why game can't start const waitingForStartCondition = - teamRedLength != teamBlueLength || - teamRedLength + teamBlueLength < 2; + redTeamLength != blueTeamLength || + redTeamLength + blueTeamLength < 2; let startConditionMessage = ""; if (waitingForStartCondition) { - if (teamRedLength + teamBlueLength < 2) { + if (redTeamLength + blueTeamLength < 2) { startConditionMessage = "Waiting for players..."; - } else if (teamRedLength != teamBlueLength) { + } else if (redTeamLength != blueTeamLength) { startConditionMessage = "Teams must be balanced..."; } } @@ -203,31 +212,27 @@ export default async function update(state) { : "Start", type: "action", action: start, - disabled: !canStart || teamRedLength != teamBlueLength, + disabled: !canStart || redTeamLength != blueTeamLength, }); } } - - if (canStart) { - - // Show options to select team buildings - htmlBlock += ` + // Show options to select team buildings + htmlBlock += `

Select Team Buildings

🔴 Team 🔴

${getBuildingKindSelectHtml( - state, - redBuildingTopId, - "buildingKindIdRed" - )} + state, + redBuildingTopId, + "buildingKindIdRed" + )}

🔵 Team 🔵

${getBuildingKindSelectHtml( - state, - blueBuildingTopId, - "buildingKindIdBlue" - )} + state, + blueBuildingTopId, + "buildingKindIdBlue" + )} `; - } if (gameActive) { // Display selected team buildings @@ -254,14 +259,14 @@ export default async function update(state) { } - // Reset is always offered (requires some trust!) - buttonList.push({ - text: "Reset", - type: "action", - action: reset, - disabled: false, - }); } + // Reset is always offered (requires some trust!) + buttonList.push({ + text: "Reset", + type: "action", + action: reset, + disabled: false, + }); console.log({ htmlBlock }) return { @@ -301,20 +306,20 @@ function getHQData(selectedBuilding) { selectedBuilding, "buildingKindIdBlue" ); - const teamRedLength = getDataInt(selectedBuilding, "teamRedLength"); - const teamBlueLength = getDataInt(selectedBuilding, "teamBlueLength"); + const redTeamLength = getDataInt(selectedBuilding, "redTeamLength"); + const blueTeamLength = getDataInt(selectedBuilding, "blueTeamLength"); return { gameActive, buildingKindIdRed, buildingKindIdBlue, - teamRedLength, - teamBlueLength, + redTeamLength, + blueTeamLength, }; } function getHQTeamUnit(selectedBuilding, team, index) { - return getDataBytes24(selectedBuilding, `team${team}Unit_${index}`); + return getDataBytes24(selectedBuilding, `${team}TeamUnit_${index}`); } // search the buildings list ofr the display buildings we're gpoing to use diff --git a/contracts/src/example-plugins/MOBA/MOBA.sol b/contracts/src/example-plugins/MOBA/MOBA.sol index 9ca8a1c95..730ae49a1 100644 --- a/contracts/src/example-plugins/MOBA/MOBA.sol +++ b/contracts/src/example-plugins/MOBA/MOBA.sol @@ -96,6 +96,8 @@ contract MOBA is BuildingKind { keccak256(abi.encodePacked("blue")) ) { processTeam(dispatcher, buildingId, "blueTeam", blueTeam, unitId); + } else { + revert("invalid team"); } } @@ -153,6 +155,8 @@ contract MOBA is BuildingKind { 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(