Skip to content

bigbizze/metis-nfts-docs

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

44 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

metis-nfts

A fully on-chain NFT game


changes from refactor of first version:

MoreMissilesPlz.sol has been removed, we now primarily deal with the UfoInvasion.sol contract, and MissileMaker.sol contract, but also the WorldLeader.sol contract when it comes to the Biden / Putin NFTs.


Latest Rinkeby Contract Addresses

WorldLeader: 0xAdC166631145abCe289C5ba9B5AD4D260CB9b9CA

MissileMaker 0x803e6F74c8Ed2f0444Ad3104B359802a1450c500

UfoInvasion: 0x1e0a0b2f493a25e728CDCb93E561c9667B9c56B2


Intro


initialize the contract like

// typescript
const worldLeaderContract = new web3.eth.Contract(
  worldLeaderAbi.abi as any,
  worldLeaderAddress
);
const missileMakerContract = new web3.eth.Contract(
  missileMakerAbi.abi as any,
  missileMakerAddress
);
const ufoInvasionContract = new web3.eth.Contract(
  ufoInvasionContract.abi as any,
  ufoInvsaionAddress
);

here are some helpers for removing all the garbage from solidity data queries and event responses:

// typescript
const getNamedValsFromObj = <T>(obj: T): T =>
  Object.assign({}, ...Object.entries(obj)
    .filter(([key,]) => key.length > 0 && Number.isNaN(Number(key[0])))
    .map(([key, val]) => ({[key]: (typeof val === "string" && !val.startsWith("0x") && !Number.isNaN(Number(val)) ? Number(val) : val)}))
  );

type HasReturnValsProp<T> = { returnValues: T };

const hasReturnVals = <T>(obj: T | HasReturnValsProp<T>): obj is HasReturnValsProp<T> =>
  (obj as any).hasOwnProperty("returnValues");

const getNamedProps = <T>(events: T[] | HasReturnValsProp<T>[]) =>
  events.map(x => hasReturnVals(x) ? x.returnValues : x).map(getNamedValsFromObj);

WorldLeader.sol & Minting


mint new NFTs like this:

const mintNftMethod = worldLeaderMintContract.methods.mintNFTs(amountToMint);

const receipt = await mintNftMethod
  .send({
    from: walletAddress,
    to: worldLeaderMintAddress,
    gas: await mintNftMethod.estimateGas(),
    nonce: await web3.eth.getTransactionCount(walletAddress, "pending"),
    chainId: 1088
  });

you can check the status of the release like this:

enum ReleaseStatus {
  Unreleased,
  Whitelisted,
  Released
}

const curReleaseStatus = await worldLeaderMintContract.methods.getReleaseStatus()
  .call({ from: walletAddress, value: "0x0" });

if (curReleaseStatus === ReleaseStatus.Whitelisted) {
  // ..
}

MissileMaker.sol & Rolling for missiles


// solidity
function maybeGetMissiles(uint randVal) external;
// typescript

const numMissilesReadyToRoll = await missileMakerContract.methods.numMissilesReadyToRoll()
  .call({ from: walletAddress, value: "0x00" });
      
if (numMissilesReadyToRoll > 0) {
    const maybeGetMissileMethod = missileMakerContract.methods.maybeGetMissiles(getRandomInt(1, 100000));
    const receipt = await maybeGetMissileMethod
      .send({
        from: walletAddress,
        to: missileMakerAddress,
        gas: await maybeGetNukeMethod.estimateGas(),
        nonce: await web3.eth.getTransactionCount(walletAddress, "pending"),
        chainId: 1088
      });
}

// solidity
event MissilesCreated(uint missileCreatedEventId, address createdForAddress);

The MissilesCreated event on MissileMaker.sol contains a missileCreatedEventId prop which can be used to query more complex state.

// solidity
struct MissileCreatedState {
    uint16 dmg;
    uint32 missileCreatedEventId;
    address owner;
    uint missileNftId;
}

function getMissileCreatedInfo(uint missileCreatedEventId) public view returns (MissileCreatedState memory);
// typescript
type MissileCreated = {
  dmg: number,
  missileCreatedEventId: number,
  address: string,
  missileNftId: number
};

const getMissileCreatedInfoFromEvent = async (walletAddress: string) => {
  const missilesCreatedEvents = getNamedProps(await missileMakerContract.getPastEvents("MissilesCreated", { fromBlock: 0 }))
    .filter(x => x.createdForAddress === walletAddress);
  const data: MissileCreated[] = [];
  for (const { missileCreatedEventId, createdForAddress } of missilesCreatedEvents) {
    data.push(
      await missileMakerContract.methods.getMissileCreatedInfo(missileCreatedEventId)
        .call({ from: walletAddress, value: "0x0" })
    );
  }
  return getNamedProps(data);
};

UfoInvasion.sol & Attacking UFOs

trying to attack a UFO before there are > 100 WorldLeader NFTs minted will fail

//solidity
function attackRandomUFOs(uint randVal, uint[] memory missileIds, uint amountUFOs) external;

attack amountUFOs random UFOs using missileIds missiles (max is 5 missiles)

// get all the missiles the user owns (this might be better as the user selecting which ones to use themselves)
const userMissiles = (await missileMakerContract.methods.getUserMissiles(walletAddress)
  .call({ from: walletAddress, value: "0x00" }))
  .map((x: string) => Number(x));
  
const attackRandomUfoMethod = missileMakerContract.methods.attackRandomUFOs(
  getRandomInt(1, 1000),
  userMissiles.slice(0, 4), // first 5 missiles
  3 // amountOfUFOs 
);

const receipt = await attackRandomUfoMethod
  .send({
    from: walletAddress,
    to: missileMakerAddress,
    gas: await attackRandomUfoMethod.estimateGas(),
    nonce: await web3.eth.getTransactionCount(walletAddress, "pending"),
    chainId: 1088
  });

Start a new UFO invasion game (this should be called by a client when they receive a GameOver) event don't worry about multiple clients calling it at the same time, _gameActive acts as a mutex for ensuring it's only called once at a time. The player who starts the game will be given 5% of the total UFO HP of the game they created as score as a reward for paying to start the game.

// solidity
function startNewUfoInvasionGame(uint randVal) public;
// typescript
const isGameActive = await ufoInvasionContract.methods.isGameActive()
  .call({ from: walletAddress, value: "0x0" });

if (!isGameActive) {
    const startNewUfoInvasionGameMethod = ufoInvasionContract.methods.startNewUfoInvasionGame(getRandomInt(1, 10000));
    const receipt = await startNewUfoInvasionGameMethod
      .send({
        from: walletAddress,
        to: ufoInvasionContract,
        gas: await startNewUfoInvasionGameMethod.estimateGas(),
        nonce: await web3.eth.getTransactionCount(walletAddress, "pending"),
        chainId: 1088
      });
}

The MissileAttackedUFO event contains a missileTxnId prop which will be the same for missiles used within the same txn, and the other two are self-explanatory.

// solidity

event MissileAttackedUFO(uint32 gameNum, address attacker, uint missileTxnId, uint missileId);

You can use the missileId given by this event with the getMissileAttackInfo method on UfoInvasion.sol to query more complex state about the missile attack event.

// solidity
struct MissileAttack {
    uint16 dmg;
    uint16 hpBefore;
    uint16 hpAfter;
    uint32 missileTxnId;
    uint32 gameNum;
    address attacker;
    address locationAddress;
    uint missileId;
    uint ufoId;
}

function getMissileAttackInfo(uint missileId) public view returns (MissileAttack memory);
type MissileAttack = {
  gameNum: number,
  missileTxnId: number,
  missileId: number,
  ufoId: number,
  attacker: string,
  dmg: number,
  hpBefore: number,
  hpAfter: number
}

export const getMissileAttackInfoFromEvent = async (walletAddress: string) => {
  // in case we only want missile attacks for a specific user, but u shud prob be subscribing to events for that anyway
  const missileAttacks = getNamedProps(await ufoInvasionContract.getPastEvents("MissileAttackedUFO", { fromBlock: 0 }))
    .filter(x => x.attacker === walletAddress);
  const data: MissileAttack[] = [];
  for (const { missileId, missileTxnId } of missileAttacks) {
    data.push(
      await ufoInvasionContract.methods.getMissileAttackInfo(missileId)
        .call({ from: walletAddress, value: "0x0" })
    );
  }
  return getNamedProps(data);
};

The GameOver event fires when every UFO in the game reaches 0 hp and includes just the gameNumber of the match.

// solidity
event GameOver(uint gameNum);

You can use the gameNumber given by this event with the getGameStatsByGameNum method on UfoInvasion.sol to query more complex state about the game which has just finished.

// solidity
struct GameStats {
    bool isOver;
    uint16 totalUfoHp;
    uint32 gameNum;
    address winner;
    uint gameStartTimeInSeconds;
    uint elapsedSecs;
    uint[] ufoIds;
}

function getGameStatsByGameNum(uint gameNum) public view returns (GameStats memory);
type GameStats = {
  gameNum: number,
  isOver: boolean,
  winner: string
  gameStartTimeInSeconds: number,
  ufoIds: number[]
};

export const getGameStatsFromGameOverEvent = async (walletAddress: string): Promise<GameStats | void> => {
  const gameOverEvents = getNamedProps(await ufoInvasionContract.getPastEvents("GameOver", { fromBlock: 0 }));
  if (gameOverEvents.length === 0 || !gameOverEvents[0].hasOwnProperty("gameNum")) {
    return console.log("got no game over events!");
  }
  return getNamedProps(
    await ufoInvasionContract.methods.getGameStatsByGameNum(gameOverEvents[0].gameNum)
      .call({ from: walletAddress, value: "0x0" })
  );
};

within each GameStats object, there is an array of ufoIds for the ids of UFOs from that game.

the following function returns information about the current game's UFOs in the case its second argument "gameNum" is undefined, otherwise it uses the "gameNum" value to determine which game's UFOs it should get information about

// solidity
struct UfoState {
    uint16 curHp;
    uint16 startingHp;
    uint32 gameNum;
    address locationAddress;
    uint ufoId;
}
function getUfoAtIdxByGameNum(uint ufoIdx, uint gameNum) external view returns (UfoState memory);
function getUfoAtIdxInCurrentGame(uint ufoIdx) external view returns (UfoState memory);
// typescript
type GameUfo = {
  locationAddress: string,
  ufoId: number,
  curHp: number,
  gameNum: number.
  startingHp: number,
  gameNumber: number
};

const getGameUFOs = async (
  walletAddress: string,
  gameNum?: number
): Promise<GameUfo[]> => {
  const gameNumUfos =
    Number(
      !gameNum
        ? await ufoInvasionContract.methods.getCurGameNumUFOs()
          .call({ from: walletAddress, value: "0x00" })
        : await ufoInvasionContract.methods.getNumUFOsInGameByGameNum(gameNum)
          .call({ from: walletAddress, value: "0x00" })
    );
  const data: GameUfo[] = [];
  for (let i = 0; i < gameNumUfos; i++) {
    data.push(
      !gameNum 
        ? await ufoInvasionContract.methods.getUfoAtIdxInCurrentGame(i)
          .call({ from: walletAddress, value: "0x00" })
        : await ufoInvasionContract.methods.getUfoAtIdxByGameNum(i, gameNum)
          .call({ from: walletAddress, value: "0x00" })
    )
  }
  return getNamedProps(data);
};

you could use thie function to retrieve the information of every game's UFO like this

const getAllGamesUFOs = async (
  walletAddress: string
) => {
  const totalNumberOfGames = Number(
    await ufoInvasionContract.methods.getTotalNumberOfGames()
      .call({ from: walletAddress, value: "0x00" })
  );
  const data: {[gameNum: number]: GameUfo[]} = {};
  for (let i = 0; i < totalNumberOfGames; i++) {
    data[i] = await getGameUFOs(walletAddress, i);
  }
  return data;
};

here are the UfoInvasion.sol data querying methods:

// solidity

// returns whether the UFO with the id `ufoId` is alive or not
function ufoIsAlive(uint ufoId) public view returns (bool);

// returns whether there is currently a game active or not
function isGameActive() external view returns (bool);

// returns the number of UFOs in the game index by `gameNum`
function getNumUFOsInGameByGameNum(uint gameNum) external view returns;

// returns information about the UFO at the index `ufoIdx` for the game number `gameNum`
function getUfoAtIdxByGameNum(uint ufoIdx, uint gameNum) external view returns (UfoState memory);

// calls getNumUFOsInGameByGameNum(_totalNumGamesPlayed)
function getCurGameNumUFOs() external view returns (uint);

// calls getUfoAtIdxByGameNum(ufoIdx, _totalNumGamesPlayed);
function getUfoAtIdxInCurrentGame(uint ufoIdx) external view returns (UfoState memory);

// returns the number of players in the current game's stats 
function getCurGameNumPlayers() external view returns (uint);

// returns the stats for the player at the index `idx` for the current game
function getCurGamePlayerAtIdx(uint idx) external view returns (CurGameScore memory);

// returns the total number of players who have stats on the leaderboard
function getNumLeaderboardPlayers() external view returns (uint);

// returns the leaderboard information for the player at the index `idx`
function getLeaderboardPlayerAtIdx(uint idx) external view returns (AllTimeLeaderboard memory);

// returns the total number of games played so far
function getTotalNumberOfGames() public view returns (uint);

// returns the game stats for the game with the idx `gameNum`
function getGameStatsByGameNum(uint gameNum) public view returns (GameStats memory);

// returns the total hp for all UFOs in the current or last game
function getTotalHpForUFOs() public view returns (uint);

here are the MissileMaker.sol data querying methods:

// solidity

// returns the number of missiles rolling attempts the message sender has available currently 
function numMissilesReadyToRoll() public view returns (uint);

// returns the amount of damage the missile with the id `missileId` does
function getMissileDmg(uint256 missileId) public view returns (uint64);

// returns the missile ids of the missiles owned by the user
function getUserMissiles(address userAddr) external view returns (uint[] memory);

// returns the percentage chance each World Leader NFT has at rolling a missile
function getMissilePercChance() public view returns (uint);

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published