diff --git a/eth/contracts/DFDiamond.sol b/eth/contracts/DFDiamond.sol index ab2050ab..9a1be719 100644 --- a/eth/contracts/DFDiamond.sol +++ b/eth/contracts/DFDiamond.sol @@ -13,9 +13,9 @@ import {ERC165, IERC165, ERC165Storage} from "@solidstate/contracts/introspectio import {DiamondBase, DiamondBaseStorage} from "@solidstate/contracts/proxy/diamond/base/DiamondBase.sol"; import {DiamondReadable, IDiamondReadable} from "@solidstate/contracts/proxy/diamond/readable/DiamondReadable.sol"; import {DiamondWritable, IDiamondWritable} from "@solidstate/contracts/proxy/diamond/writable/DiamondWritable.sol"; -import {IERC721} from "@solidstate/contracts/token/ERC721/IERC721.sol"; -import {IERC721Metadata} from "@solidstate/contracts/token/ERC721/metadata/IERC721Metadata.sol"; -import {IERC721Enumerable} from "@solidstate/contracts/token/ERC721/enumerable/IERC721Enumerable.sol"; +import {IERC1155} from "@solidstate/contracts/token/ERC1155/IERC1155.sol"; +import {IERC1155Metadata} from "@solidstate/contracts/token/ERC1155/metadata/IERC1155Metadata.sol"; +import {IERC1155Enumerable} from "@solidstate/contracts/token/ERC1155/enumerable/IERC1155Enumerable.sol"; /** * @title SolidState "Diamond" proxy reference implementation @@ -58,9 +58,9 @@ contract DFDiamond is DiamondBase, DiamondReadable, DiamondWritable, Ownable, ER erc165.setSupportedInterface(type(IERC173).interfaceId, true); // Store ERC721 interface - erc165.setSupportedInterface(type(IERC721).interfaceId, true); - erc165.setSupportedInterface(type(IERC721Metadata).interfaceId, true); - erc165.setSupportedInterface(type(IERC721Enumerable).interfaceId, true); + erc165.setSupportedInterface(type(IERC1155).interfaceId, true); + erc165.setSupportedInterface(type(IERC1155Metadata).interfaceId, true); + erc165.setSupportedInterface(type(IERC1155Enumerable).interfaceId, true); // register Diamond diff --git a/eth/contracts/DFInitialize.sol b/eth/contracts/DFInitialize.sol index 3ce6a411..b0033910 100644 --- a/eth/contracts/DFInitialize.sol +++ b/eth/contracts/DFInitialize.sol @@ -21,7 +21,7 @@ pragma solidity ^0.8.0; // Interface imports // Inherited storage -import {ERC721MetadataStorage} from "@solidstate/contracts/token/ERC721/metadata/ERC721MetadataStorage.sol"; +import {ERC1155MetadataStorage} from "@solidstate/contracts/token/ERC1155/metadata/ERC1155MetadataStorage.sol"; // Library imports import {WithStorage, SpaceshipConstants} from "./libraries/LibStorage.sol"; @@ -99,7 +99,7 @@ struct InitArgs { } contract DFInitialize is WithStorage { - using ERC721MetadataStorage for ERC721MetadataStorage.Layout; + using ERC1155MetadataStorage for ERC1155MetadataStorage.Layout; // You can add parameters to this function in order to pass in // data to set initialize state variables @@ -108,11 +108,8 @@ contract DFInitialize is WithStorage { string memory artifactBaseURI, InitArgs memory initArgs ) external { - // Setup the ERC721 metadata - // TODO(#1925): Add name and symbol for the artifact tokens - ERC721MetadataStorage.layout().name = ""; - ERC721MetadataStorage.layout().symbol = ""; - ERC721MetadataStorage.layout().baseURI = artifactBaseURI; + // Setup the ERC1155 metadata + ERC1155MetadataStorage.layout().baseURI = artifactBaseURI; gs().diamondAddress = address(this); diff --git a/eth/contracts/DFTypes.sol b/eth/contracts/DFTypes.sol index 34c55522..198648a5 100644 --- a/eth/contracts/DFTypes.sol +++ b/eth/contracts/DFTypes.sol @@ -71,6 +71,12 @@ struct Planet { uint256 invadeStartBlock; address capturer; uint256 locationId; + // Token stuff + uint256[] artifacts; + uint256[] spaceships; + uint256 activeArtifact; + uint256 wormholeTo; + uint256 artifactActivationTime; } struct RevealedCoords { @@ -205,7 +211,11 @@ enum ArtifactType { PlanetaryShield, PhotoidCannon, BloomFilter, - BlackDomain, + BlackDomain +} + +enum SpaceshipType { + Unknown, ShipMothership, ShipCrescent, ShipWhale, @@ -222,33 +232,6 @@ enum ArtifactRarity { Mythic } -// for NFTs -struct Artifact { - bool isInitialized; - uint256 id; - uint256 planetDiscoveredOn; - ArtifactRarity rarity; - Biome planetBiome; - uint256 mintedAtTimestamp; - address discoverer; - ArtifactType artifactType; - // an artifact is 'activated' iff lastActivated > lastDeactivated - uint256 activations; - uint256 lastActivated; - uint256 lastDeactivated; - uint256 wormholeTo; // location id - address controller; // space ships can be controlled regardless of which planet they're on -} - -// for artifact getters -struct ArtifactWithMetadata { - Artifact artifact; - Upgrade upgrade; - Upgrade timeDelayedUpgrade; // for photoid canons specifically. - address owner; - uint256 locationId; // 0 if planet is not deposited into contract or is on a voyage - uint256 voyageId; // 0 is planet is not deposited into contract or is on a planet -} enum Biome { Unknown, @@ -263,3 +246,44 @@ enum Biome { Lava, Corrupted } + +enum TokenType { + Unknown, + Artifact, + Spaceship, + Silver +} + +enum ArtifactInfo { + Unknown, + TokenType, + ArtifactRarity, + ArtifactType, + Biome +} + +struct Artifact { + uint256 id; + TokenType tokenType; + ArtifactRarity rarity; + ArtifactType artifactType; + Biome planetBiome; +} + +// Used for accessing properties of spaceship tokenId +enum SpaceshipInfo { + Unknown, + TokenType, + SpaceshipType +} + +struct Spaceship { + uint256 id; + TokenType tokenType; + SpaceshipType spaceshipType; +} + +enum SilverInfo { + Unknown, + TokenType +} diff --git a/eth/contracts/Tokens.md b/eth/contracts/Tokens.md new file mode 100644 index 00000000..15c792a7 --- /dev/null +++ b/eth/contracts/Tokens.md @@ -0,0 +1,240 @@ +# Dark Forest Tokens (ERC1155): + +Each token in Dark Forest is a collection under the ERC1155 Standard. + +Each collection has a `tokenId` can be fungible (supply > 1) or non-fungible (supply = 1). + +The fundamental data structure in ERC1155 is `mapping(uint256 => mapping(address => uint256)) balances;`. + +`balances[tokenId][myAddress]` = number of tokens I have of a given collection. + +The `uint256` `tokenId`, which identifies a _set_ of tokens, is represented in the following way: + +Each `uint256` `tokenId` is broken down into 32 chunks of 8 bits each (32\*8 = 256). + +> | chunk1 | chunk 2 | ... chunk32 |. + +Chunks are used from left to right, so a token that has a value of `0xff` in chunk1 looks like`0xff00000000000000000000000000000000000000000000000000000000000000` + +Another way to visualize the `tokenId` is by highlighting each chunk: `0x**ff**_00_**00**_00_**00**_00_...` + +Each chunk allows for 2^8 (256) unique pieces of information. If you need more than 256 properties +of a token, you can use an additional chunk. + +For example, if you wanted to add a new property to an Artifact called `Weather`, you use the next +chunk(s) to encode that value. You would have 256 options for what `Weather` an Artifact could have. + +This architecture allows us to encode information about a Dark Forest token in the `tokenId` itself, +and, more importantly, it allows to create a copy of a token just by using the same `tokenId`. + +This concept will become clearer as we examine how these rules are used for Artifacts and +Spaceships. + +Each of these chunks has 256 options. So you can have 256 Artifact Rarities, Types, Biomes etc... +This should be plenty, but if you need more you just use another chunk. + +## General Token Info + +Each collection (called a Token) in Dark Forest must have a Library dedicated to it with the naming +convention `Lib.sol`. + +The first byte (from left) of the tokenId **must** correspond to the appropriate value in the +`TokenType` struct in `DFTypes.sol` + +```js +enum TokenType { + Unknown, + Artifact, // 0x01 = Artifact + Spaceship // 0x02 = Spaceship + // etc... +} +``` + +The `Lib.sol`. file **must** have the following methods: + +1. `encode() returns (uint256 tokenId)` +2. `decode(uint256 tokenId) returns ()` +3. `is returns (bool)` + +where `` can be a struct (like Artifacts or Spaceships) or just a uint256 (like Silver). + +Additionally methods can be added to each library, but they must be `internal` functions that can be +inlined into other facets or libraries. + +## Artifacts + +In Dark Forest, artifacts are fungible. If I have two Epic Monoliths found in the Ocean biome, I can +sell either one and they have the same buffs for planets. + +Thus, we can represent in this information with the following encoding: + +> | TokenType.Artifact | ArtifactRarity.Epic | ArtifactType.Monolith | Biome.Ocean | ... | + +In hex: + +> | 0x01 | 0x03| 0x01 | 0x01 | ... + +Under the hood, we simply calculate the value of the given property (Epic Artifact = 3), convert it +to hex (0x03), and place it in the appropriate location (ArtifactInfo.ArtifactRarity = 2, so place 0x03 +two chunks from the left). + +To decode, we just reverse this process. Given the id `0x010301010000.....`, I know that it +is an Epic Monoliths Artifact from the Ocean Biome. + +You can see the actual encoding and decoding take place in `LibArtifact.sol/encode` and +`LibArtifact.sol/decode`. + +### Minting + +In Dark Forest, when Artifacts are minted, they are placed on the planet they were found on, _but +they are still owned by the core game contract_. From the eyes of the game contract, it will just +own bunch of Epic Monoliths. However, we have an additional data structure, +`mapping(uint256 =>uint256[]) planetArtifacts;` in the contract storage that keeps track of which +artifacts are on which planet. + +If you own the planet that contains an Artifact, you can move that Artifact. This means that players +can lose access to their artifacts if the planet it is on gets captured. + +### Withdrawing + +For players to gain ownership over their Aritifacts, they must withdraw them from a Spacetime Rip. + +Under the hood, this decreases the number of Epic Monoliths owned by the core game contract by 1 and +increases the number of Epic Monoliths owned by the player by 1. It also removes _one_ instance of +the Epic Monolith tokenId from the `planetArtifacts` mapping. + +Players can now transfer or burn the Artifact. + +### Transferring + +ERC1155 Transfers function just like ERC721 transfers, but can also specify an amount of tokens to +transfer. + +If I wanted to send _two_ Epic Monoliths to a friend, I would call the following: +`safeTransferFrom(me,myFriend,epicMonolithId, 2, "")` + +## Spaceships + +Spaceships are very similar to Artifacts, in that they give special powers to planets. + +Spaceships are _non-fungible_. If I have two Epic Monoliths on a planet and you capture the +planet, you control the Monoliths. However, if I have my Mothership on your planet and you have your Mothership on +your planet, you cannot control my Mothership. This means that the `planetArtifacts` mapping above +will fail to enforce this primary rule of Spaceships. To fix this, each Spaceship must have a unique +id and be owned by the account that minted it. + +To differentiate between Spaceships and Artifacts, we have an additional data structure, +`mapping(uint256 =>uint256[]) planetSpaceships;` in the contract storage that keeps track of which +spaceships are on which planet. + +However, we can still use our `tokenId` chunk system because we don't need all 256 bits to uniquely +identify a Spaceship. We limit Spaceships to the first 16 chunks (128 bits) and save 128 for a +unique id. This uses the [Split-Id](https://eips.ethereum.org/EIPS/eip-1155#split-id-bits) method +recommended in the ERC1155 Proposal. + +A Mothership Spaceship is represented like so + +> | TokenType.Spaceship | SpaceshipType.ShipMothership | ... chunk 16 | uniqueId (16 chunks) | + +In hex: + +> | 0x02 | 0x01 ... | uniqueId (16 chunks) | + +A Spaceship's tokenId = `` + +### Minting + +When a user creates or mints a Spaceship, they own it, not the contract. + +Lets say my Mothership has id `<0x02000a00><0x01>`. + +`balances[0x02000a00...01][myAddress] = 1`. + +If Velorum mints their own Mothership, it would have id: `<0x02000a00><0x02>`. + +`balances[0x02000a00...01][myAddress] = 1`. + +Velorum's Mothership has the same TokenInfo, but a unique identifier at the end. This means the +contract stores my Mothership and Velorum's Mothership as completely different collections. +However, because our ships share the same first 128 bits, we can still calculate the information +about the Spaceship (SpaceshipType, TokenType) just by feeding the `LibSpaceship.decode` function the `tokenId`. + +### Transferring + +Right now, Spaceships cannot be transferred. This means players cannot sell their powerful ships. +This is implemented using the `beforeTokenTransfer` hook in ERC1155, which only lets the core +contract transfer Spaceships. This is used when minting a ship. + +We could easily turn off this check if we wanted players to be able to buy and sell Spaceships. + +## Activating + +### Artifacts + +Every Artifact and one Spaceship (Crescent) must be activated on a planet to be used. + +The fungible nature of Artifacts creates a challenge: How do we associate data with specific +artifacts? How do I know when my Epic Monolith is activated? + +There are new data structures in `LibStorage.sol` to handle this information. Because Artifact +activations are always associated with planets, we can store the needed information on the relevant planets +instead of on the Artifacts themselves. + +```js + mapping(uint256 => uint256[]) planetArtifacts; + mapping(uint256 => uint256) planetActiveArtifact; + mapping(uint256 => uint256) planetWormholes; + mapping(uint256 => uint256) planetArtifactActivationTime; +``` + +Lets say I move my Epic Monolith with id `0xMonolith` to planet A with id `0xA`. +Now `planetArtifacts[0xA]` includes `0xMonolith`. +Now, I activate my Monolith. The following occurs: + +- `planetActiveArtifact[0xA] = 0xMonolith`. +- `planetArtifactActivationTime[0xA] = block.timestamp` + +If I had a Wormhole instead of a Monolith, I would also update `planetWormholes`. Lets say I want a +wormhole from `0xA` to `0xB`. + +- `planetWormholes[0xA] = 0xB` + +### Spaceships + +- The only Spaceship that can be activated is the Crescent, and that is burned in the same + transaction. This means that a Spaceship Id will never be added or removed to the + `planetActiveArtifact` mapping. +- This defines a fundamental difference between Spaceships and Artifacts: Spaceship effects are + always applied on arrival / departure, or are instanenous. There is (at the moment) no concept of + activating them. + +## Deactivating + +If I deactivate my artifact from `0xA`, we simply undo these maneuvers: + +- `planetActiveArtifact[0xA] = 0`. +- `planetArtifactActivationTime[0xA] = 0` + +For the wormhole, we do the same: + +- `planetWormholes[0xA] = 0` + +## Simulteanous Activate and Deactivate + +Some Artifacts (Bloom Filters) and Spaceships (Crescents) are burned on use. + +All we do is make sure that we call the Activate and Deactivate functions in the same transaction. + +## Photoid + +For Photoid cannons, we simply apply the Photoid move if the current time is greater than the time +activated + the Photoid activation delay. If someone captures the planet, the activation time +doesn't change. + +# Next Steps + +## Silver + +Silver is a fungible token with TokenType = 3. Although silver on planets is multiplied by 1000 for +precision, Silver the token is exact. As such, any function that converts from silver on planets to +Silver must divide by 1000. diff --git a/eth/contracts/facets/DFAdminFacet.sol b/eth/contracts/facets/DFAdminFacet.sol index 19efb8fa..f34a052b 100644 --- a/eth/contracts/facets/DFAdminFacet.sol +++ b/eth/contracts/facets/DFAdminFacet.sol @@ -2,22 +2,30 @@ pragma solidity ^0.8.0; // Library imports -import {LibPermissions} from "../libraries/LibPermissions.sol"; +import {LibArtifact} from "../libraries/LibArtifact.sol"; +import {LibArtifactUtils} from "../libraries/LibArtifactUtils.sol"; import {LibGameUtils} from "../libraries/LibGameUtils.sol"; +import {LibPermissions} from "../libraries/LibPermissions.sol"; import {LibPlanet} from "../libraries/LibPlanet.sol"; -import {LibArtifactUtils} from "../libraries/LibArtifactUtils.sol"; +import {LibSpaceship} from "../libraries/LibSpaceship.sol"; // Storage imports import {WithStorage} from "../libraries/LibStorage.sol"; +// Contract imports +import {DFArtifactFacet} from "./DFArtifactFacet.sol"; + // Type imports -import {SpaceType, DFPInitPlanetArgs, AdminCreatePlanetArgs, Artifact, ArtifactType, Player, Planet} from "../DFTypes.sol"; +import {Artifact, SpaceType, Spaceship, SpaceshipType, DFPInitPlanetArgs, AdminCreatePlanetArgs, DFTCreateArtifactArgs, ArtifactType, Player, Planet, TokenType} from "../DFTypes.sol"; +import "hardhat/console.sol"; contract DFAdminFacet is WithStorage { event AdminOwnershipChanged(uint256 loc, address newOwner); event AdminPlanetCreated(uint256 loc); - event AdminGiveSpaceship(uint256 loc, address owner, ArtifactType artifactType); + event AdminGiveSpaceship(uint256 loc, address owner, SpaceshipType shipType); event PauseStateChanged(bool paused); + event AdminArtifactCreated(address player, uint256 artifactId, uint256 loc); + event AdminArtifactActivated(address player, uint256 artifactId, uint256 loc); ///////////////////////////// /// Administrative Engine /// @@ -136,20 +144,19 @@ contract DFAdminFacet is WithStorage { function adminGiveSpaceShip( uint256 locationId, address owner, - ArtifactType artifactType + SpaceshipType shipType ) public onlyAdmin { require(gs().planets[locationId].isInitialized, "planet is not initialized"); - require(LibArtifactUtils.isSpaceship(artifactType), "artifact type must be a space ship"); - uint256 shipId = LibArtifactUtils.createAndPlaceSpaceship(locationId, owner, artifactType); - Artifact memory artifact = gs().artifacts[shipId]; + uint256 shipId = LibArtifactUtils.createAndPlaceSpaceship(locationId, owner, shipType); + Spaceship memory spaceship = LibSpaceship.decode(shipId); Planet memory planet = gs().planets[locationId]; - planet = LibPlanet.applySpaceshipArrive(artifact, planet); + planet = LibPlanet.applySpaceshipArrive(spaceship, planet); gs().planets[locationId] = planet; - emit AdminGiveSpaceship(locationId, owner, artifactType); + emit AdminGiveSpaceship(locationId, owner, shipType); } function adminInitializePlanet(uint256 locationId, uint256 perlin) public onlyAdmin { @@ -161,4 +168,48 @@ contract DFAdminFacet is WithStorage { function setPlanetTransferEnabled(bool enabled) public onlyAdmin { gameConstants().PLANET_TRANSFER_ENABLED = enabled; } + + function adminGiveArtifact(DFTCreateArtifactArgs memory args) public onlyAdmin { + // Note: calling this in tests should supply Diamond address as args.owner + uint256 tokenId = LibArtifact.create(args.rarity, args.artifactType, args.biome); + + Artifact memory artifact = DFArtifactFacet(address(this)).createArtifact( + tokenId, + args.owner + ); + + // Don't put artifact on planet if no planetId given. + if (args.planetId != 0) LibArtifact.putArtifactOnPlanet(args.planetId, artifact.id); + emit AdminArtifactCreated(args.owner, artifact.id, args.planetId); + } + + // function adminGiveAndActivateArtifact(DFTCreateArtifactArgs memory args, uint256 wormholeTo) + // public + // onlyAdmin + // { + // // Note: calling this in tests should supply Diamond address as args.owner + // uint256 tokenId = LibArtifact.create(args.rarity, args.artifactType, args.biome); + + // Artifact memory artifact = DFArtifactFacet(address(this)).createArtifact( + // tokenId, + // args.owner + // ); + + // console.log("owner", args.owner); + // // Don't put artifact on planet if no planetId given. + // console.log("wormholeto,", wormholeTo); + // console.log("planetId,", wormholeTo); + // console.log("planet is intialized?", gs().planets[args.planetId].isInitialized); + // if (args.planetId != 0) + // // address(this).delegatecall(abi.encodeWithSignature("transferOwnership(address)", address(0))); + // address(this).delegatecall( + // abi.encodeWithSignature( + // "activateArtifact(uint256, uint256, uint256)", + // args.planetId, + // artifact.id, + // wormholeTo + // ) + // ); + // emit AdminArtifactActivated(args.owner, artifact.id, args.planetId); + // } } diff --git a/eth/contracts/facets/DFArtifactFacet.sol b/eth/contracts/facets/DFArtifactFacet.sol index 35f93bc7..4c5cf10e 100644 --- a/eth/contracts/facets/DFArtifactFacet.sol +++ b/eth/contracts/facets/DFArtifactFacet.sol @@ -2,25 +2,27 @@ pragma solidity ^0.8.0; // Contract imports -import {SolidStateERC721} from "@solidstate/contracts/token/ERC721/SolidStateERC721.sol"; -import {ERC721BaseStorage} from "@solidstate/contracts/token/ERC721/base/ERC721BaseStorage.sol"; import {DFVerifierFacet} from "./DFVerifierFacet.sol"; import {DFWhitelistFacet} from "./DFWhitelistFacet.sol"; +import {DFTokenFacet} from "./DFTokenFacet.sol"; // Library Imports import {LibPermissions} from "../libraries/LibPermissions.sol"; import {LibGameUtils} from "../libraries/LibGameUtils.sol"; import {LibArtifactUtils} from "../libraries/LibArtifactUtils.sol"; +import {LibArtifact} from "../libraries/LibArtifact.sol"; +import {LibSpaceship} from "../libraries/LibSpaceship.sol"; + import {LibPlanet} from "../libraries/LibPlanet.sol"; // Storage imports import {WithStorage} from "../libraries/LibStorage.sol"; // Type imports -import {Artifact, ArtifactType, DFTCreateArtifactArgs, DFPFindArtifactArgs} from "../DFTypes.sol"; +import {Artifact, ArtifactRarity, ArtifactType, Biome, TokenType, DFTCreateArtifactArgs, DFPFindArtifactArgs, Spaceship, SpaceshipType} from "../DFTypes.sol"; +import "hardhat/console.sol"; -contract DFArtifactFacet is WithStorage, SolidStateERC721 { - using ERC721BaseStorage for ERC721BaseStorage.Layout; +contract DFArtifactFacet is WithStorage { event PlanetProspected(address player, uint256 loc); event ArtifactFound(address player, uint256 artifactId, uint256 loc); @@ -48,90 +50,87 @@ contract DFArtifactFacet is WithStorage, SolidStateERC721 { _; } - modifier onlyAdminOrCore() { + modifier onlyAdmin() { require( - msg.sender == gs().diamondAddress || msg.sender == LibPermissions.contractOwner(), - "Only the Core or Admin addresses can fiddle with artifacts." + msg.sender == LibPermissions.contractOwner(), + "Only Admin address can perform this action." ); _; } - modifier onlyAdmin() { + modifier onlyAdminOrCore() { require( - msg.sender == LibPermissions.contractOwner(), - "Only Admin address can perform this action." + msg.sender == gs().diamondAddress || msg.sender == LibPermissions.contractOwner(), + "Only the Core or Admin addresses can fiddle with artifacts." ); _; } - function createArtifact(DFTCreateArtifactArgs memory args) + function createArtifact(uint256 tokenId, address owner) public onlyAdminOrCore returns (Artifact memory) { - require(args.tokenId >= 1, "artifact id must be positive"); - - _mint(args.owner, args.tokenId); - - Artifact memory newArtifact = Artifact( - true, - args.tokenId, - args.planetId, - args.rarity, - args.biome, - block.timestamp, - args.discoverer, - args.artifactType, - 0, - 0, - 0, - 0, - args.controller - ); + require(tokenId >= 1, "token id must be positive"); + require(LibArtifact.isArtifact(tokenId), "token must be Artifact"); + // Account, Id, Amount, Data + DFTokenFacet(address(this)).mint(owner, tokenId, 1); - gs().artifacts[args.tokenId] = newArtifact; - - return newArtifact; + return LibArtifact.decode(tokenId); } - function getArtifact(uint256 tokenId) public view returns (Artifact memory) { - return gs().artifacts[tokenId]; + function tokenIsOwnedBy(address owner, uint256 tokenId) public view returns (bool) { + return DFTokenFacet(address(this)).balanceOf(owner, tokenId) > 0; } - function getArtifactAtIndex(uint256 idx) public view returns (Artifact memory) { - return gs().artifacts[tokenByIndex(idx)]; - } + function createSpaceship(uint256 tokenId, address owner) + public + onlyAdminOrCore + returns (Spaceship memory) + { + require(tokenId >= 1, "token id must be positive"); + require(LibSpaceship.isShip(tokenId), "token must be Spaceship"); - function getPlayerArtifactIds(address playerId) public view returns (uint256[] memory) { - uint256 balance = balanceOf(playerId); - uint256[] memory results = new uint256[](balance); + // Account, Id, Amount, Data + DFTokenFacet(address(this)).mint(owner, tokenId, 1); - for (uint256 idx = 0; idx < balance; idx++) { - results[idx] = tokenOfOwnerByIndex(playerId, idx); - } + return getSpaceshipFromId(tokenId); + } - return results; + function getSpaceshipFromId(uint256 shipId) public pure returns (Spaceship memory) { + return LibSpaceship.decode(shipId); } - function transferArtifact(uint256 tokenId, address newOwner) public onlyAdminOrCore { - if (newOwner == address(0)) { - _burn(tokenId); - } else { - _transfer(ownerOf(tokenId), newOwner, tokenId); - } + function createSpaceshipId(SpaceshipType spaceshipType) public pure returns (uint256) { + return LibSpaceship.create(spaceshipType); } - function updateArtifact(Artifact memory updatedArtifact) public onlyAdminOrCore { - require( - ERC721BaseStorage.layout().exists(updatedArtifact.id), - "you cannot update an artifact that doesn't exist" - ); + function createArtifactId( + ArtifactRarity rarity, + ArtifactType artifactType, + Biome biome + ) public pure returns (uint256) { + return LibArtifact.create(rarity, artifactType, biome); + } - gs().artifacts[updatedArtifact.id] = updatedArtifact; + function getArtifactFromId(uint256 artifactId) public pure returns (Artifact memory) { + return LibArtifact.decode(artifactId); } - function doesArtifactExist(uint256 tokenId) public view returns (bool) { - return ERC721BaseStorage.layout().exists(tokenId); + // This calls the low level _transfer call which doesn't check if the msg.sender actually owns + // the tokenId. TODO: See if this is a problem. + function transferArtifact( + uint256 tokenId, + address owner, + address newOwner + ) public onlyAdminOrCore { + if (newOwner == address(0)) { + // account, id, amount. + DFTokenFacet(address(this)).burn(owner, tokenId, 1); + } else { + // sender receiver id amount data + DFTokenFacet(address(this)).transfer(owner, owner, newOwner, tokenId, 1, ""); + } } function findArtifact( @@ -164,27 +163,6 @@ contract DFArtifactFacet is WithStorage, SolidStateERC721 { emit ArtifactFound(msg.sender, foundArtifactId, planetId); } - function depositArtifact(uint256 locationId, uint256 artifactId) public notPaused { - // should this be implemented as logic that is triggered when a player sends - // an artifact to the contract with locationId in the extra data? - // might be better use of the ERC721 standard - can use safeTransfer then - LibPlanet.refreshPlanet(locationId); - - LibArtifactUtils.depositArtifact(locationId, artifactId, address(this)); - - emit ArtifactDeposited(msg.sender, artifactId, locationId); - } - - // withdraws the given artifact from the given planet. you must own the planet, - // the artifact must be on the given planet - function withdrawArtifact(uint256 locationId, uint256 artifactId) public notPaused { - LibPlanet.refreshPlanet(locationId); - - LibArtifactUtils.withdrawArtifact(locationId, artifactId); - - emit ArtifactWithdrawn(msg.sender, artifactId, locationId); - } - // activates the given artifact on the given planet. the artifact must have // been previously deposited on this planet. the artifact cannot be activated // within a certain cooldown period, depending on the artifact type @@ -240,7 +218,7 @@ contract DFArtifactFacet is WithStorage, SolidStateERC721 { uint256 id1 = LibArtifactUtils.createAndPlaceSpaceship( locationId, owner, - ArtifactType.ShipMothership + SpaceshipType.ShipMothership ); emit ArtifactFound(msg.sender, id1, locationId); } @@ -249,7 +227,7 @@ contract DFArtifactFacet is WithStorage, SolidStateERC721 { uint256 id2 = LibArtifactUtils.createAndPlaceSpaceship( locationId, owner, - ArtifactType.ShipCrescent + SpaceshipType.ShipCrescent ); emit ArtifactFound(msg.sender, id2, locationId); } @@ -258,7 +236,7 @@ contract DFArtifactFacet is WithStorage, SolidStateERC721 { uint256 id3 = LibArtifactUtils.createAndPlaceSpaceship( locationId, owner, - ArtifactType.ShipWhale + SpaceshipType.ShipWhale ); emit ArtifactFound(msg.sender, id3, locationId); } @@ -267,7 +245,7 @@ contract DFArtifactFacet is WithStorage, SolidStateERC721 { uint256 id4 = LibArtifactUtils.createAndPlaceSpaceship( locationId, owner, - ArtifactType.ShipGear + SpaceshipType.ShipGear ); emit ArtifactFound(msg.sender, id4, locationId); } @@ -276,7 +254,7 @@ contract DFArtifactFacet is WithStorage, SolidStateERC721 { uint256 id5 = LibArtifactUtils.createAndPlaceSpaceship( locationId, owner, - ArtifactType.ShipTitan + SpaceshipType.ShipTitan ); emit ArtifactFound(msg.sender, id5, locationId); @@ -284,12 +262,4 @@ contract DFArtifactFacet is WithStorage, SolidStateERC721 { gs().players[msg.sender].claimedShips = true; } - - function adminGiveArtifact(DFTCreateArtifactArgs memory args) public onlyAdmin { - Artifact memory artifact = createArtifact(args); - transferArtifact(artifact.id, address(this)); - LibGameUtils._putArtifactOnPlanet(artifact.id, args.planetId); - - emit ArtifactFound(args.owner, artifact.id, args.planetId); - } } diff --git a/eth/contracts/facets/DFGetterFacet.sol b/eth/contracts/facets/DFGetterFacet.sol index 93ff1c38..6c545777 100644 --- a/eth/contracts/facets/DFGetterFacet.sol +++ b/eth/contracts/facets/DFGetterFacet.sol @@ -5,14 +5,17 @@ pragma solidity ^0.8.0; import {DFArtifactFacet} from "./DFArtifactFacet.sol"; // Library imports -import {LibPermissions} from "../libraries/LibPermissions.sol"; +import {LibArtifact} from "../libraries/LibArtifact.sol"; +import {LibArtifactUtils} from "../libraries/LibArtifactUtils.sol"; import {LibGameUtils} from "../libraries/LibGameUtils.sol"; +import {LibPermissions} from "../libraries/LibPermissions.sol"; +import {LibSpaceship} from "../libraries/LibSpaceship.sol"; // Storage imports import {WithStorage, SnarkConstants, GameConstants} from "../libraries/LibStorage.sol"; // Type imports -import {RevealedCoords, ArrivalData, Planet, PlanetEventType, PlanetEventMetadata, PlanetDefaultStats, PlanetData, Player, ArtifactWithMetadata, Upgrade, Artifact} from "../DFTypes.sol"; +import {RevealedCoords, Artifact, ArrivalData, Planet, PlanetEventType, PlanetEventMetadata, PlanetDefaultStats, PlanetData, Player, Upgrade, Spaceship} from "../DFTypes.sol"; contract DFGetterFacet is WithStorage { // FIRST-LEVEL GETTERS - mirrors the solidity autogenerated toplevel getters, but for GameStorage @@ -76,14 +79,6 @@ contract DFGetterFacet is WithStorage { return gs().revealedCoords[key]; } - function artifactIdToPlanetId(uint256 key) public view returns (uint256) { - return gs().artifactIdToPlanetId[key]; - } - - function artifactIdToVoyageId(uint256 key) public view returns (uint256) { - return gs().artifactIdToVoyageId[key]; - } - function planetEvents(uint256 key) public view returns (PlanetEventMetadata[] memory) { return gs().planetEvents[key]; } @@ -96,8 +91,12 @@ contract DFGetterFacet is WithStorage { return gs().planetArrivals[key]; } - function planetArtifacts(uint256 key) public view returns (uint256[] memory) { - return gs().planetArtifacts[key]; + function planetArtifacts(uint256 locationId) public view returns (uint256[] memory) { + return gs().planets[locationId].artifacts; + } + + function planetSpaceships(uint256 locationId) public view returns (uint256[] memory) { + return gs().planets[locationId].spaceships; } // ADDITIONAL UTILITY GETTERS @@ -315,91 +314,135 @@ contract DFGetterFacet is WithStorage { return ret; } - function getArtifactById(uint256 artifactId) - public - view - returns (ArtifactWithMetadata memory ret) - { - Artifact memory artifact = DFArtifactFacet(address(this)).getArtifact(artifactId); + function getArtifactActivationTimeOnPlanet(uint256 locationId) public view returns (uint256) { + return gs().planets[locationId].artifactActivationTime; + } - address owner; + // function getArtifactById(uint256 artifactId) + // public + // view + // returns (ArtifactWithMetadata memory ret) + // { + // Artifact memory artifact = LibArtifactUtils(address(this)).decodeArtifact(artifactId); - try DFArtifactFacet(address(this)).ownerOf(artifact.id) returns (address addr) { - owner = addr; - } catch Error(string memory) { - // artifact is probably burned / owned by 0x0, so owner is 0x0 - } catch (bytes memory) { - // this shouldn't happen - } + // address owner; + + // try DFArtifactFacet(address(this)).ownerOf(artifact.id) returns (address addr) { + // owner = addr; + // } catch Error(string memory) { + // // artifact is probably burned / owned by 0x0, so owner is 0x0 + // } catch (bytes memory) { + // // this shouldn't happen + // } - ret = ArtifactWithMetadata({ - artifact: artifact, - upgrade: LibGameUtils._getUpgradeForArtifact(artifact), - timeDelayedUpgrade: LibGameUtils.timeDelayUpgrade(artifact), - owner: owner, - locationId: gs().artifactIdToPlanetId[artifact.id], - voyageId: gs().artifactIdToVoyageId[artifact.id] - }); + // ret = ArtifactWithMetadata({ + // artifact: artifact, + // upgrade: LibGameUtils._getUpgradeForArtifact(artifact), + // timeDelayedUpgrade: LibGameUtils.timeDelayUpgrade(artifact), + // owner: owner, + // locationId: gs().artifactIdToPlanetId[artifact.id], + // voyageId: gs().artifactIdToVoyageId[artifact.id] + // }); + // } + + function getUpgradeForArtifact(uint256 artifactId) public pure returns (Upgrade memory) { + return LibArtifact.getUpgradeForArtifact(LibArtifact.decode(artifactId)); } - function getArtifactsOnPlanet(uint256 locationId) - public - view - returns (ArtifactWithMetadata[] memory ret) - { - uint256[] memory artifactIds = gs().planetArtifacts[locationId]; - ret = new ArtifactWithMetadata[](artifactIds.length); + function getArtifactsOnPlanet(uint256 locationId) public view returns (Artifact[] memory ret) { + uint256[] memory artifactIds = gs().planets[locationId].artifacts; + ret = new Artifact[](artifactIds.length); for (uint256 i = 0; i < artifactIds.length; i++) { - ret[i] = getArtifactById(artifactIds[i]); + ret[i] = LibArtifact.decode(artifactIds[i]); } return ret; } - function bulkGetPlanetArtifacts(uint256[] calldata planetIds) + function getSpaceshipsOnPlanet(uint256 locationId) public view - returns (ArtifactWithMetadata[][] memory) + returns (Spaceship[] memory ret) { - ArtifactWithMetadata[][] memory ret = new ArtifactWithMetadata[][](planetIds.length); + uint256[] memory tokenIds = gs().planets[locationId].spaceships; + ret = new Spaceship[](tokenIds.length); + for (uint256 i = 0; i < tokenIds.length; i++) { + ret[i] = LibSpaceship.decode(tokenIds[i]); + } + return ret; + } - for (uint256 i = 0; i < planetIds.length; i++) { - uint256[] memory planetOwnedArtifactIds = gs().planetArtifacts[planetIds[i]]; - ret[i] = bulkGetArtifactsByIds(planetOwnedArtifactIds); + // Combo on Ships and Artifacts + function tokenExistsOnPlanet(uint256 locationId, uint256 tokenId) public view returns (bool) { + bool hasToken = false; + uint256[] memory artifactIds = gs().planets[locationId].artifacts; + for (uint256 i = 0; i < artifactIds.length; i++) { + if (artifactIds[i] == tokenId) return true; } + uint256[] memory shipIds = gs().planets[locationId].spaceships; + for (uint256 i = 0; i < shipIds.length; i++) { + if (shipIds[i] == tokenId) return true; + } + return hasToken; + } - return ret; + function hasActiveArtifact(uint256 locationId) public view returns (bool) { + return LibArtifact.hasActiveArtifact(locationId); } - function bulkGetArtifactsByIds(uint256[] memory ids) + function getActiveArtifactOnPlanet(uint256 locationId) public view - returns (ArtifactWithMetadata[] memory ret) + returns (Artifact memory ret) { - ret = new ArtifactWithMetadata[](ids.length); - - for (uint256 i = 0; i < ids.length; i++) { - Artifact memory artifact = DFArtifactFacet(address(this)).getArtifact(ids[i]); - - address owner; - - try DFArtifactFacet(address(this)).ownerOf(artifact.id) returns (address addr) { - owner = addr; - } catch Error(string memory) { - // artifact is probably burned or owned by 0x0, so owner is 0x0 - } catch (bytes memory) { - // this shouldn't happen - } - - ret[i] = ArtifactWithMetadata({ - artifact: artifact, - upgrade: LibGameUtils._getUpgradeForArtifact(artifact), - timeDelayedUpgrade: LibGameUtils.timeDelayUpgrade(artifact), - owner: owner, - locationId: gs().artifactIdToPlanetId[artifact.id], - voyageId: gs().artifactIdToVoyageId[artifact.id] - }); - } - } + uint256 artifactId = gs().planets[locationId].activeArtifact; + return LibArtifact.decode(artifactId); + } + + // function bulkGetPlanetArtifacts(uint256[] calldata planetIds) + // public + // view + // returns (ArtifactWithMetadata[][] memory) + // { + // ArtifactWithMetadata[][] memory ret = new ArtifactWithMetadata[][](planetIds.length); + + // for (uint256 i = 0; i < planetIds.length; i++) { + // uint256[] memory planetOwnedArtifactIds = gs().planetArtifacts[planetIds[i]]; + // ret[i] = bulkGetArtifactsByIds(planetOwnedArtifactIds); + // } + + // return ret; + // } + + // function bulkGetArtifactsByIds(uint256[] memory ids) + // public + // view + // returns (ArtifactWithMetadata[] memory ret) + // { + // ret = new ArtifactWithMetadata[](ids.length); + + // for (uint256 i = 0; i < ids.length; i++) { + // Artifact memory artifact = LibArtifactUtils.decodeArtifact(ids[i]); + + // address owner; + + // try DFArtifactFacet(address(this)).ownerOf(artifact.id) returns (address addr) { + // owner = addr; + // } catch Error(string memory) { + // // artifact is probably burned or owned by 0x0, so owner is 0x0 + // } catch (bytes memory) { + // // this shouldn't happen + // } + + // ret[i] = ArtifactWithMetadata({ + // artifact: artifact, + // upgrade: LibGameUtils._getUpgradeForArtifact(artifact), + // timeDelayedUpgrade: LibGameUtils.timeDelayUpgrade(artifact), + // owner: owner, + // locationId: gs().artifactIdToPlanetId[artifact.id], + // voyageId: gs().artifactIdToVoyageId[artifact.id] + // }); + // } + // } /** * Get a group or artifacts based on their index, fetch all between startIdx & endIdx. @@ -408,32 +451,32 @@ contract DFGetterFacet is WithStorage { * @param startIdx index of the first element to get * @param endIdx index of the last element to get */ - function bulkGetArtifacts(uint256 startIdx, uint256 endIdx) - public - view - returns (ArtifactWithMetadata[] memory ret) - { - ret = new ArtifactWithMetadata[](endIdx - startIdx); - - for (uint256 i = startIdx; i < endIdx; i++) { - Artifact memory artifact = DFArtifactFacet(address(this)).getArtifactAtIndex(i); - address owner = address(0); - - try DFArtifactFacet(address(this)).ownerOf(artifact.id) returns (address addr) { - owner = addr; - } catch Error(string memory) { - // artifact is probably burned or owned by 0x0, so owner is 0x0 - } catch (bytes memory) { - // this shouldn't happen - } - ret[i - startIdx] = ArtifactWithMetadata({ - artifact: artifact, - upgrade: LibGameUtils._getUpgradeForArtifact(artifact), - timeDelayedUpgrade: LibGameUtils.timeDelayUpgrade(artifact), - owner: owner, - locationId: gs().artifactIdToPlanetId[artifact.id], - voyageId: gs().artifactIdToVoyageId[artifact.id] - }); - } - } + // function bulkGetArtifacts(uint256 startIdx, uint256 endIdx) + // public + // view + // returns (ArtifactWithMetadata[] memory ret) + // { + // ret = new ArtifactWithMetadata[](endIdx - startIdx); + + // for (uint256 i = startIdx; i < endIdx; i++) { + // Artifact memory artifact = LibArtifactUtils(address(this)).decodeArtifactAtIndex(i); + // address owner = address(0); + + // try DFArtifactFacet(address(this)).ownerOf(artifact.id) returns (address addr) { + // owner = addr; + // } catch Error(string memory) { + // // artifact is probably burned or owned by 0x0, so owner is 0x0 + // } catch (bytes memory) { + // // this shouldn't happen + // } + // ret[i - startIdx] = ArtifactWithMetadata({ + // artifact: artifact, + // upgrade: LibGameUtils._getUpgradeForArtifact(artifact), + // timeDelayedUpgrade: LibGameUtils.timeDelayUpgrade(artifact), + // owner: owner, + // locationId: gs().artifactIdToPlanetId[artifact.id], + // voyageId: gs().artifactIdToVoyageId[artifact.id] + // }); + // } + // } } diff --git a/eth/contracts/facets/DFMoveFacet.sol b/eth/contracts/facets/DFMoveFacet.sol index f358fb79..443e3784 100644 --- a/eth/contracts/facets/DFMoveFacet.sol +++ b/eth/contracts/facets/DFMoveFacet.sol @@ -3,18 +3,21 @@ pragma solidity ^0.8.0; // Contract imports import {DFVerifierFacet} from "./DFVerifierFacet.sol"; +import {DFArtifactFacet} from "./DFArtifactFacet.sol"; // Library imports +import {LibArtifact} from "../libraries/LibArtifact.sol"; import {ABDKMath64x64} from "../vendor/libraries/ABDKMath64x64.sol"; import {LibGameUtils} from "../libraries/LibGameUtils.sol"; import {LibArtifactUtils} from "../libraries/LibArtifactUtils.sol"; import {LibPlanet} from "../libraries/LibPlanet.sol"; +import {LibSpaceship} from "../libraries/LibSpaceship.sol"; // Storage imports import {WithStorage} from "../libraries/LibStorage.sol"; // Type imports -import {ArrivalData, ArrivalType, Artifact, ArtifactType, DFPCreateArrivalArgs, DFPMoveArgs, Planet, PlanetEventMetadata, PlanetEventType, Upgrade} from "../DFTypes.sol"; +import {ArrivalData, ArrivalType, Artifact, ArtifactRarity, ArtifactType, DFPCreateArrivalArgs, DFPMoveArgs, Planet, PlanetEventMetadata, PlanetEventType, Spaceship, SpaceshipType, Upgrade} from "../DFTypes.sol"; contract DFMoveFacet is WithStorage { modifier notPaused() { @@ -117,7 +120,9 @@ contract DFMoveFacet is WithStorage { arrivalType = ArrivalType.Wormhole; } - if (!_isSpaceshipMove(args)) { + if (_isSpaceshipMove(args)) { + _removeSpaceshipEffectsFromOriginPlanet(args.oldLoc, args.movedArtifactId); + } else if (_isArtifactMove(args)) { (bool photoidPresent, Upgrade memory newTempUpgrade) = _checkPhotoid(args); if (photoidPresent) { temporaryUpgrade = newTempUpgrade; @@ -125,8 +130,6 @@ contract DFMoveFacet is WithStorage { } } - _removeSpaceshipEffectsFromOriginPlanet(args); - uint256 popMoved = args.popMoved; uint256 silverMoved = args.silverMoved; uint256 remainingOriginPlanetPopulation = gs().planets[args.oldLoc].population - popMoved; @@ -149,6 +152,7 @@ contract DFMoveFacet is WithStorage { _transferPlanetSpaceJunkToPlayer(args); } + // updates oldLoc speed if photoid. LibGameUtils._buffPlanet(args.oldLoc, temporaryUpgrade); uint256 travelTime = effectiveDistTimesHundred / gs().planets[args.oldLoc].speed; @@ -197,7 +201,7 @@ contract DFMoveFacet is WithStorage { require(args.popMoved == 0, "ship moves must move 0 energy"); require(args.silverMoved == 0, "ship moves must move 0 silver"); require( - gs().artifacts[args.movedArtifactId].controller == msg.sender, + DFArtifactFacet(address(this)).tokenIsOwnedBy(msg.sender, args.movedArtifactId), "you can only move your own ships" ); } else { @@ -223,13 +227,15 @@ contract DFMoveFacet is WithStorage { if (args.movedArtifactId != 0) { require( - gs().planetArtifacts[args.newLoc].length < 5, - "too many artifacts on this planet" + gs().planets[args.newLoc].artifacts.length + + gs().planets[args.newLoc].spaceships.length < + 5, + "too many tokens on this planet" ); } } - function applySpaceshipDepart(Artifact memory artifact, Planet memory planet) + function applySpaceshipDepart(Spaceship memory spaceship, Planet memory planet) public view returns (Planet memory) @@ -238,21 +244,21 @@ contract DFMoveFacet is WithStorage { return planet; } - if (artifact.artifactType == ArtifactType.ShipMothership) { + if (spaceship.spaceshipType == SpaceshipType.ShipMothership) { if (planet.energyGroDoublers == 1) { planet.energyGroDoublers--; planet.populationGrowth /= 2; } else if (planet.energyGroDoublers > 1) { planet.energyGroDoublers--; } - } else if (artifact.artifactType == ArtifactType.ShipWhale) { + } else if (spaceship.spaceshipType == SpaceshipType.ShipWhale) { if (planet.silverGroDoublers == 1) { planet.silverGroDoublers--; planet.silverGrowth /= 2; } else if (planet.silverGroDoublers > 1) { planet.silverGroDoublers--; } - } else if (artifact.artifactType == ArtifactType.ShipTitan) { + } else if (spaceship.spaceshipType == SpaceshipType.ShipTitan) { // so that updating silver/energy starts from the current time, // as opposed to the last time that the planet was updated planet.lastUpdated = block.timestamp; @@ -265,10 +271,10 @@ contract DFMoveFacet is WithStorage { /** Undo the spaceship effects that were applied when the ship arrived on the planet. */ - function _removeSpaceshipEffectsFromOriginPlanet(DFPMoveArgs memory args) private { - Artifact memory movedArtifact = gs().artifacts[args.movedArtifactId]; - Planet memory planet = applySpaceshipDepart(movedArtifact, gs().planets[args.oldLoc]); - gs().planets[args.oldLoc] = planet; + function _removeSpaceshipEffectsFromOriginPlanet(uint256 originLoc, uint256 shipId) private { + Spaceship memory spaceship = LibSpaceship.decode(shipId); + Planet memory planet = applySpaceshipDepart(spaceship, gs().planets[originLoc]); + gs().planets[originLoc] = planet; } /** @@ -281,30 +287,41 @@ contract DFMoveFacet is WithStorage { view returns (bool wormholePresent, uint256 effectiveDistModifier) { - Artifact memory relevantWormhole; - Artifact memory activeArtifactFrom = LibGameUtils.getActiveArtifact(args.oldLoc); - Artifact memory activeArtifactTo = LibGameUtils.getActiveArtifact(args.newLoc); - - // TODO: take the greater rarity of these, or disallow wormholes between planets that - // already have a wormhole between them - if ( - activeArtifactFrom.isInitialized && - activeArtifactFrom.artifactType == ArtifactType.Wormhole && - activeArtifactFrom.wormholeTo == args.newLoc - ) { - relevantWormhole = activeArtifactFrom; - } else if ( - activeArtifactTo.isInitialized && - activeArtifactTo.artifactType == ArtifactType.Wormhole && - activeArtifactTo.wormholeTo == args.oldLoc - ) { - relevantWormhole = activeArtifactTo; + wormholePresent = false; + ArtifactRarity wormholeRarity = ArtifactRarity.Unknown; + + // Check from Loc + if (LibArtifact.hasActiveArtifact(args.oldLoc)) { + Artifact memory activeArtifactFrom = LibArtifact.getActiveArtifact(args.oldLoc); + // If active artifact is a Wormhole and destination is newLoc + if ( + activeArtifactFrom.artifactType == ArtifactType.Wormhole && + gs().planets[args.oldLoc].wormholeTo == args.newLoc + ) { + wormholeRarity = activeArtifactFrom.rarity; + wormholePresent = true; + } + } + // Check to loc + if (LibArtifact.hasActiveArtifact(args.newLoc)) { + Artifact memory activeArtifactTo = LibArtifact.getActiveArtifact(args.newLoc); + // If active artifact is a Wormhole and destination is fromLoc + if ( + activeArtifactTo.artifactType == ArtifactType.Wormhole && + gs().planets[args.newLoc].wormholeTo == args.oldLoc + ) { + // Ensures higher rarity wormhole will be used. + // TODO: Make sure client knows this. + if (activeArtifactTo.rarity > wormholeRarity) { + wormholeRarity = activeArtifactTo.rarity; + } + wormholePresent = true; + } } - if (relevantWormhole.isInitialized) { - wormholePresent = true; + if (wormholePresent) { uint256[6] memory speedBoosts = [uint256(1), 2, 4, 8, 16, 32]; - effectiveDistModifier = speedBoosts[uint256(relevantWormhole.rarity)]; + effectiveDistModifier = speedBoosts[uint256(wormholeRarity)]; } } @@ -317,16 +334,17 @@ contract DFMoveFacet is WithStorage { private returns (bool photoidPresent, Upgrade memory temporaryUpgrade) { - Artifact memory activeArtifactFrom = LibGameUtils.getActiveArtifact(args.oldLoc); - if ( - activeArtifactFrom.isInitialized && - activeArtifactFrom.artifactType == ArtifactType.PhotoidCannon && - block.timestamp - activeArtifactFrom.lastActivated >= - gameConstants().PHOTOID_ACTIVATION_DELAY - ) { - photoidPresent = true; - LibArtifactUtils.deactivateArtifact(args.oldLoc); - temporaryUpgrade = LibGameUtils.timeDelayUpgrade(activeArtifactFrom); + if (LibArtifact.hasActiveArtifact(args.oldLoc)) { + Artifact memory activeArtifactFrom = LibArtifact.getActiveArtifact(args.oldLoc); + if ( + activeArtifactFrom.artifactType == ArtifactType.PhotoidCannon && + block.timestamp - gs().planets[args.oldLoc].artifactActivationTime >= + gameConstants().PHOTOID_ACTIVATION_DELAY + ) { + photoidPresent = true; + LibArtifactUtils.deactivateArtifact(args.oldLoc); + temporaryUpgrade = LibGameUtils.timeDelayUpgrade(activeArtifactFrom); + } } } @@ -402,22 +420,27 @@ contract DFMoveFacet is WithStorage { } } - function _isSpaceshipMove(DFPMoveArgs memory args) private view returns (bool) { - return LibArtifactUtils.isSpaceship(gs().artifacts[args.movedArtifactId].artifactType); + function _isSpaceshipMove(DFPMoveArgs memory args) private pure returns (bool) { + return LibSpaceship.isShip(args.movedArtifactId); + } + + function _isArtifactMove(DFPMoveArgs memory args) private pure returns (bool) { + return LibArtifact.isArtifact(args.movedArtifactId); } function _createArrival(DFPCreateArrivalArgs memory args) private { // enter the arrival data for event id Planet memory planet = gs().planets[args.oldLoc]; + uint256 popArriving = _getDecayedPop( args.popMoved, args.effectiveDistTimesHundred, planet.range, planet.populationCap ); - bool isSpaceship = LibArtifactUtils.isSpaceship( - gs().artifacts[args.movedArtifactId].artifactType - ); + + bool isSpaceship = LibSpaceship.isShip(args.movedArtifactId); + // space ship moves are implemented as 0-energy moves require(popArriving > 0 || isSpaceship, "Not enough forces to make move"); require(isSpaceship ? args.popMoved == 0 : true, "spaceship moves must be 0 energy moves"); @@ -434,10 +457,18 @@ contract DFMoveFacet is WithStorage { carriedArtifactId: args.movedArtifactId, distance: args.actualDist }); - + // Photoids are burned in _checkPhotoid, so don't remove twice if (args.movedArtifactId != 0) { - LibGameUtils._takeArtifactOffPlanet(args.movedArtifactId, args.oldLoc); - gs().artifactIdToVoyageId[args.movedArtifactId] = gs().planetEventsCount; + if (isSpaceship) { + LibSpaceship.takeSpaceshipOffPlanet(args.oldLoc, args.movedArtifactId); + } else if (LibArtifact.isArtifact(args.movedArtifactId)) { + Artifact memory artifact = LibArtifact.decode(args.movedArtifactId); + if (artifact.artifactType != ArtifactType.PhotoidCannon) { + LibArtifact.takeArtifactOffPlanet(args.oldLoc, args.movedArtifactId); + } + } else { + require(false, "cannot move token of this type"); + } } } diff --git a/eth/contracts/facets/DFTokenFacet.sol b/eth/contracts/facets/DFTokenFacet.sol new file mode 100644 index 00000000..338fbc59 --- /dev/null +++ b/eth/contracts/facets/DFTokenFacet.sol @@ -0,0 +1,109 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.0; + +// Contract imports +import {SolidStateERC1155} from "@solidstate/contracts/token/ERC1155/SolidStateERC1155.sol"; + +// Library Imports +import {LibPermissions} from "../libraries/LibPermissions.sol"; +import {LibSilver} from "../libraries/LibSilver.sol"; +import {LibSpaceship} from "../libraries/LibSpaceship.sol"; + +// Storage imports +import {WithStorage} from "../libraries/LibStorage.sol"; + +import "hardhat/console.sol"; + +contract DFTokenFacet is WithStorage, SolidStateERC1155 { + modifier onlyAdminOrCore() { + require( + msg.sender == gs().diamondAddress || msg.sender == LibPermissions.contractOwner(), + "Only the Core or Admin addresses can fiddle with tokens." + ); + _; + } + + function _beforeTokenTransfer( + address operator, + address from, + address to, + uint256[] memory ids, + uint256[] memory amounts, + bytes memory data + ) internal virtual override { + uint256 length = ids.length; + for (uint256 i = 0; i < length; i++) { + // Only core contract can transfer Spaceships + if (LibSpaceship.isShip(ids[i])) { + require(msg.sender == gs().diamondAddress, "player cannot transfer a Spaceship"); + } + } + } + + /** + * @notice set per-token metadata URI + * @param tokenId token whose metadata URI to set + * @param tokenURI per-token URI + */ + function setTokenURI(uint256 tokenId, string memory tokenURI) public { + _setTokenURI(tokenId, tokenURI); + } + + /** + * @notice ERC1155 mint + * @param owner of new tokens + * @param tokenId tokenId to mint + * @param amount amount of tokens to mint + */ + function mint( + address owner, + uint256 tokenId, + uint256 amount + ) public onlyAdminOrCore { + _mint(owner, tokenId, amount, ""); + } + + /** + * @notice burn given quantity of tokens held by given address + * @param account holder of tokens to burn + * @param id token ID + * @param amount quantity of tokens to burn + */ + function burn( + address account, + uint256 id, + uint256 amount + ) public onlyAdminOrCore { + _burn(account, id, amount); + } + + /** + * @notice transfer tokens between given addresses + * @dev ERC1155Receiver implementation is not checked + * @param operator executor of transfer + * @param sender sender of tokens + * @param recipient receiver of tokens + * @param id token ID + * @param amount quantity of tokens to transfer + * @param data data payload + */ + function transfer( + address operator, + address sender, + address recipient, + uint256 id, + uint256 amount, + bytes memory data + ) public onlyAdminOrCore { + _transfer(operator, sender, recipient, id, amount, data); + } + + function tokenExists(address owner, uint256 tokenId) public view returns (bool) { + return balanceOf(owner, tokenId) > 0; + } + + function getSilverBalance(address player) public view returns (uint256) { + uint256 silverId = LibSilver.create(); + return balanceOf(player, silverId); + } +} diff --git a/eth/contracts/libraries/LibArtifact.sol b/eth/contracts/libraries/LibArtifact.sol new file mode 100644 index 00000000..d0f68736 --- /dev/null +++ b/eth/contracts/libraries/LibArtifact.sol @@ -0,0 +1,356 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.0; + +/** + * Library for all things Artifacts + */ + +// Library imports +import {LibUtils} from "./LibUtils.sol"; + +// Storage imports +import {LibStorage, GameStorage, GameConstants, SnarkConstants} from "./LibStorage.sol"; + +// Type imports +import {Artifact, ArtifactInfo, ArtifactRarity, ArtifactType, Biome, SpaceType, TokenType, Upgrade} from "../DFTypes.sol"; +import "hardhat/console.sol"; + +library LibArtifact { + function gs() internal pure returns (GameStorage storage) { + return LibStorage.gameStorage(); + } + + /** + * @notice Create the token ID for a Artifact with the following properties: + * @param _rarity Artifact + * @param _artifactType Artifact + * @param _biome Artifact + */ + function create( + ArtifactRarity _rarity, + ArtifactType _artifactType, + Biome _biome + ) internal pure returns (uint256) { + // x << y is equivalent to the mathematical expression x * 2**y + require(isValidArtifactRarity(_rarity), "artifact rarity is not valid"); + require(isValidArtifactType(_artifactType), "artifact type is not valid"); + require(isValidBiome(_biome), "artifact biome is not valid"); + + uint256 tokenType = LibUtils.shiftLeft( + uint8(TokenType.Artifact), // value + uint8(ArtifactInfo.TokenType) // chunk position in tokenId + ); + uint256 rarity = LibUtils.shiftLeft(uint8(_rarity), uint8(ArtifactInfo.ArtifactRarity)); + uint256 artifactType = LibUtils.shiftLeft( + uint8(_artifactType), + uint8(ArtifactInfo.ArtifactType) + ); + uint256 biome = LibUtils.shiftLeft(uint8(_biome), uint8(ArtifactInfo.Biome)); + return tokenType + rarity + artifactType + biome; + } + + function decode(uint256 artifactId) internal pure returns (Artifact memory) { + bytes memory _b = abi.encodePacked(artifactId); + uint8 tokenIdx = uint8(ArtifactInfo.TokenType) - 1; // account for Unknown at 0 + uint8 rarityIdx = uint8(ArtifactInfo.ArtifactRarity) - 1; + uint8 typeIdx = uint8(ArtifactInfo.ArtifactType) - 1; + uint8 biomeIdx = uint8(ArtifactInfo.Biome) - 1; + + TokenType tokenType = TokenType(LibUtils.calculateByteUInt(_b, tokenIdx, tokenIdx)); + ArtifactRarity rarity = ArtifactRarity( + LibUtils.calculateByteUInt(_b, rarityIdx, rarityIdx) + ); + ArtifactType artifactType = ArtifactType(LibUtils.calculateByteUInt(_b, typeIdx, typeIdx)); + Biome biome = Biome(LibUtils.calculateByteUInt(_b, biomeIdx, biomeIdx)); + + require(isArtifact(artifactId), "token type is not artifact"); + require(isValidArtifactRarity(rarity), "artifact rarity is not valid"); + require(isValidArtifactType(artifactType), "artifact type is not valid"); + require(isValidBiome(biome), "artifact biome is not valid"); + + return + Artifact({ + id: artifactId, + tokenType: tokenType, + rarity: rarity, + artifactType: artifactType, + planetBiome: biome + }); + } + + function isValidArtifactRarity(ArtifactRarity rarity) internal pure returns (bool) { + return (rarity >= ArtifactRarity.Common && rarity <= ArtifactRarity.Mythic); + } + + function isValidArtifactType(ArtifactType artifactType) internal pure returns (bool) { + return (artifactType >= ArtifactType.Monolith && artifactType <= ArtifactType.BlackDomain); + } + + function isValidBiome(Biome biome) internal pure returns (bool) { + return (biome >= Biome.Ocean && biome <= Biome.Corrupted); + } + + function isArtifact(uint256 tokenId) internal pure returns (bool) { + bytes memory _b = abi.encodePacked(tokenId); + uint8 tokenIdx = uint8(ArtifactInfo.TokenType) - 1; + uint8 tokenType = uint8(LibUtils.calculateByteUInt(_b, tokenIdx, tokenIdx)); + return (TokenType(tokenType) == TokenType.Artifact); + } + + function getUpgradeForArtifact(Artifact memory artifact) + internal + pure + returns (Upgrade memory) + { + if (artifact.artifactType == ArtifactType.PlanetaryShield) { + uint256[6] memory defenseMultipliersPerRarity = [uint256(100), 150, 200, 300, 450, 650]; + + return + Upgrade({ + popCapMultiplier: 100, + popGroMultiplier: 100, + rangeMultiplier: 20, + speedMultiplier: 20, + defMultiplier: defenseMultipliersPerRarity[uint256(artifact.rarity)] + }); + } + + if (artifact.artifactType == ArtifactType.PhotoidCannon) { + uint256[6] memory def = [uint256(100), 50, 40, 30, 20, 10]; + return + Upgrade({ + popCapMultiplier: 100, + popGroMultiplier: 100, + rangeMultiplier: 100, + speedMultiplier: 100, + defMultiplier: def[uint256(artifact.rarity)] + }); + } + + if (uint256(artifact.artifactType) >= 5) { + return + Upgrade({ + popCapMultiplier: 100, + popGroMultiplier: 100, + rangeMultiplier: 100, + speedMultiplier: 100, + defMultiplier: 100 + }); + } + + Upgrade memory ret = Upgrade({ + popCapMultiplier: 100, + popGroMultiplier: 100, + rangeMultiplier: 100, + speedMultiplier: 100, + defMultiplier: 100 + }); + + if (artifact.artifactType == ArtifactType.Monolith) { + ret.popCapMultiplier += 5; + ret.popGroMultiplier += 5; + } else if (artifact.artifactType == ArtifactType.Colossus) { + ret.speedMultiplier += 5; + } else if (artifact.artifactType == ArtifactType.Spaceship) { + ret.rangeMultiplier += 5; + } else if (artifact.artifactType == ArtifactType.Pyramid) { + ret.defMultiplier += 5; + } + + if (artifact.planetBiome == Biome.Ocean) { + ret.speedMultiplier += 5; + ret.defMultiplier += 5; + } else if (artifact.planetBiome == Biome.Forest) { + ret.defMultiplier += 5; + ret.popCapMultiplier += 5; + ret.popGroMultiplier += 5; + } else if (artifact.planetBiome == Biome.Grassland) { + ret.popCapMultiplier += 5; + ret.popGroMultiplier += 5; + ret.rangeMultiplier += 5; + } else if (artifact.planetBiome == Biome.Tundra) { + ret.defMultiplier += 5; + ret.rangeMultiplier += 5; + } else if (artifact.planetBiome == Biome.Swamp) { + ret.speedMultiplier += 5; + ret.rangeMultiplier += 5; + } else if (artifact.planetBiome == Biome.Desert) { + ret.speedMultiplier += 10; + } else if (artifact.planetBiome == Biome.Ice) { + ret.rangeMultiplier += 10; + } else if (artifact.planetBiome == Biome.Wasteland) { + ret.defMultiplier += 10; + } else if (artifact.planetBiome == Biome.Lava) { + ret.popCapMultiplier += 10; + ret.popGroMultiplier += 10; + } else if (artifact.planetBiome == Biome.Corrupted) { + ret.rangeMultiplier += 5; + ret.speedMultiplier += 5; + ret.popCapMultiplier += 5; + ret.popGroMultiplier += 5; + } + + uint256 scale = 1 + (uint256(artifact.rarity) / 2); + + ret.popCapMultiplier = scale * ret.popCapMultiplier - (scale - 1) * 100; + ret.popGroMultiplier = scale * ret.popGroMultiplier - (scale - 1) * 100; + ret.speedMultiplier = scale * ret.speedMultiplier - (scale - 1) * 100; + ret.rangeMultiplier = scale * ret.rangeMultiplier - (scale - 1) * 100; + ret.defMultiplier = scale * ret.defMultiplier - (scale - 1) * 100; + + return ret; + } + + function artifactRarityFromPlanetLevel(uint256 planetLevel) + internal + pure + returns (ArtifactRarity) + { + if (planetLevel <= 1) return ArtifactRarity.Common; + else if (planetLevel <= 3) return ArtifactRarity.Rare; + else if (planetLevel <= 5) return ArtifactRarity.Epic; + else if (planetLevel <= 7) return ArtifactRarity.Legendary; + else return ArtifactRarity.Mythic; + } + + // an artifact is only considered 'activated' if this method returns true. + // we do not have an `isActive` field on artifact; the times that the + // artifact was last activated and deactivated are sufficent to determine + // whether or not the artifact is activated. + + function isActivated(uint256 locationId, uint256 artifactId) internal view returns (bool) { + return (gs().planets[locationId].activeArtifact == artifactId); + } + + function isArtifactOnPlanet(uint256 locationId, uint256 artifactId) + internal + view + returns (bool) + { + for (uint256 i; i < gs().planets[locationId].artifacts.length; i++) { + if (gs().planets[locationId].artifacts[i] == artifactId) { + return true; + } + } + + return false; + } + + function randomArtifactTypeAndLevelBonus( + uint256 artifactSeed, + Biome biome, + SpaceType spaceType + ) internal pure returns (ArtifactType, uint256) { + uint256 lastByteOfSeed = artifactSeed % 0xFF; + uint256 secondLastByteOfSeed = ((artifactSeed - lastByteOfSeed) / 256) % 0xFF; + + ArtifactType artifactType = ArtifactType.Pyramid; + + if (lastByteOfSeed < 39) { + artifactType = ArtifactType.Monolith; + } else if (lastByteOfSeed < 78) { + artifactType = ArtifactType.Colossus; + } + // else if (lastByteOfSeed < 117) { + // artifactType = ArtifactType.Spaceship; + // } + else if (lastByteOfSeed < 156) { + artifactType = ArtifactType.Pyramid; + } else if (lastByteOfSeed < 171) { + artifactType = ArtifactType.Wormhole; + } else if (lastByteOfSeed < 186) { + artifactType = ArtifactType.PlanetaryShield; + } else if (lastByteOfSeed < 201) { + artifactType = ArtifactType.PhotoidCannon; + } else if (lastByteOfSeed < 216) { + artifactType = ArtifactType.BloomFilter; + } else if (lastByteOfSeed < 231) { + artifactType = ArtifactType.BlackDomain; + } else { + if (biome == Biome.Ice) { + artifactType = ArtifactType.PlanetaryShield; + } else if (biome == Biome.Lava) { + artifactType = ArtifactType.PhotoidCannon; + } else if (biome == Biome.Wasteland) { + artifactType = ArtifactType.BloomFilter; + } else if (biome == Biome.Corrupted) { + artifactType = ArtifactType.BlackDomain; + } else { + artifactType = ArtifactType.Wormhole; + } + } + + uint256 bonus = 0; + if (secondLastByteOfSeed < 4) { + bonus = 2; + } else if (secondLastByteOfSeed < 16) { + bonus = 1; + } + + return (artifactType, bonus); + } + + function putArtifactOnPlanet(uint256 locationId, uint256 artifactId) internal { + gs().planets[locationId].artifacts.push(artifactId); + } + + /** + * Remove artifactId from planet with locationId if artifactId exists AND is not active. + */ + function takeArtifactOffPlanet(uint256 locationId, uint256 artifactId) internal { + uint256 artifactsOnThisPlanet = gs().planets[locationId].artifacts.length; + + bool hadTheArtifact = false; + + for (uint256 i = 0; i < artifactsOnThisPlanet; i++) { + if (gs().planets[locationId].artifacts[i] == artifactId) { + require( + !isActivated(locationId, artifactId), + "you cannot take an activated artifact off a planet" + ); + + gs().planets[locationId].artifacts[i] = gs().planets[locationId].artifacts[ + artifactsOnThisPlanet - 1 + ]; + + hadTheArtifact = true; + break; + } + } + + require(hadTheArtifact, "this artifact was not present on this planet"); + gs().planets[locationId].artifacts.pop(); + } + + // if the given planet has an activated artifact on it, then return the artifact + // otherwise, return a 'null artifact' + function getActiveArtifact(uint256 locationId) internal view returns (Artifact memory) { + require(hasActiveArtifact(locationId), "planet does not have an active artifact"); + uint256 artifactId = gs().planets[locationId].activeArtifact; + return LibArtifact.decode(artifactId); + } + + function hasActiveArtifact(uint256 locationId) internal view returns (bool) { + uint256 artifactId = gs().planets[locationId].activeArtifact; + return artifactId != 0; + } + + // if the given artifact is on the given planet, then return the artifact + // otherwise, throw error + function getPlanetArtifact(uint256 locationId, uint256 artifactId) + internal + view + returns (Artifact memory a) + { + bool found = false; + for (uint256 i; i < gs().planets[locationId].artifacts.length; i++) { + if (gs().planets[locationId].artifacts[i] == artifactId) { + a = LibArtifact.decode(artifactId); + found = true; + return a; + } + } + + require(found, "artifact not found"); + } +} diff --git a/eth/contracts/libraries/LibArtifactUtils.sol b/eth/contracts/libraries/LibArtifactUtils.sol index 9df35d15..1e327b6a 100644 --- a/eth/contracts/libraries/LibArtifactUtils.sol +++ b/eth/contracts/libraries/LibArtifactUtils.sol @@ -3,15 +3,21 @@ pragma solidity ^0.8.0; // External contract imports import {DFArtifactFacet} from "../facets/DFArtifactFacet.sol"; +import {DFTokenFacet} from "../facets/DFTokenFacet.sol"; +import {DFGetterFacet} from "../facets/DFGetterFacet.sol"; // Library imports +import {LibArtifact} from "./LibArtifact.sol"; import {LibGameUtils} from "./LibGameUtils.sol"; +import {LibSpaceship} from "./LibSpaceship.sol"; +import {LibUtils} from "./LibUtils.sol"; // Storage imports import {LibStorage, GameStorage, GameConstants} from "./LibStorage.sol"; // Type imports -import {Biome, Planet, PlanetType, Artifact, ArtifactType, ArtifactRarity, DFPFindArtifactArgs, DFTCreateArtifactArgs} from "../DFTypes.sol"; +import {Biome, Planet, PlanetType, ArtifactType, ArtifactRarity, TokenType, DFPFindArtifactArgs, DFTCreateArtifactArgs, Artifact, ArtifactInfo, Spaceship, SpaceshipType} from "../DFTypes.sol"; +import "hardhat/console.sol"; library LibArtifactUtils { function gs() internal pure returns (GameStorage storage) { @@ -58,29 +64,14 @@ library LibArtifactUtils { function createAndPlaceSpaceship( uint256 planetId, address owner, - ArtifactType shipType + SpaceshipType shipType ) public returns (uint256) { - require(shipType <= ArtifactType.ShipTitan && shipType >= ArtifactType.ShipMothership); - - uint256 id = uint256(keccak256(abi.encodePacked(planetId, gs().miscNonce++))); - - DFTCreateArtifactArgs memory createArtifactArgs = DFTCreateArtifactArgs( - id, - msg.sender, - planetId, - ArtifactRarity.Unknown, - Biome.Unknown, - shipType, - address(this), - owner - ); + uint256 tokenId = LibSpaceship.create(shipType) + uint128(gs().miscNonce++); - Artifact memory foundArtifact = DFArtifactFacet(address(this)).createArtifact( - createArtifactArgs - ); - LibGameUtils._putArtifactOnPlanet(foundArtifact.id, planetId); + Spaceship memory spaceship = DFArtifactFacet(address(this)).createSpaceship(tokenId, owner); + LibSpaceship.putSpaceshipOnPlanet(planetId, spaceship.id); - return id; + return spaceship.id; } function findArtifact(DFPFindArtifactArgs memory args) public returns (uint256 artifactId) { @@ -100,25 +91,22 @@ library LibArtifactUtils { ) ); - (ArtifactType artifactType, uint256 levelBonus) = LibGameUtils - ._randomArtifactTypeAndLevelBonus(artifactSeed, biome, planet.spaceType); - - DFTCreateArtifactArgs memory createArtifactArgs = DFTCreateArtifactArgs( - artifactSeed, - msg.sender, - args.planetId, - LibGameUtils.artifactRarityFromPlanetLevel(levelBonus + planet.planetLevel), - biome, - artifactType, - args.coreAddress, - address(0) + (ArtifactType artifactType, uint256 levelBonus) = LibArtifact + .randomArtifactTypeAndLevelBonus(artifactSeed, biome, planet.spaceType); + + ArtifactRarity rarity = LibArtifact.artifactRarityFromPlanetLevel( + levelBonus + planet.planetLevel ); + uint256 tokenId = LibArtifact.create(rarity, artifactType, biome); + // Artifacts found in game are owned by player. Artifact memory foundArtifact = DFArtifactFacet(address(this)).createArtifact( - createArtifactArgs + tokenId, + msg.sender ); - LibGameUtils._putArtifactOnPlanet(foundArtifact.id, args.planetId); + // Artifacts are not put on planet. + // LibArtifact.putArtifactOnPlanet(args.planetId, foundArtifact.id); planet.hasTriedFindingArtifact = true; gs().players[msg.sender].score += gameConstants().ARTIFACT_POINT_VALUES[ @@ -134,41 +122,41 @@ library LibArtifactUtils { uint256 wormholeTo ) public { Planet storage planet = gs().planets[locationId]; - Artifact storage artifact = gs().artifacts[artifactId]; - require( - LibGameUtils.isArtifactOnPlanet(locationId, artifactId), - "can't active an artifact on a planet it's not on" + gs().planets[locationId].spaceships.length + gs().planets[locationId].artifacts.length < + 5, + "too many tokens on this planet" ); - - if (isSpaceship(artifact.artifactType)) { - activateSpaceshipArtifact(locationId, artifactId, planet, artifact); - } else { + // Either artifact is owned by player OR is on planet. + require( + DFTokenFacet(address(this)).tokenExists(msg.sender, artifactId) || + LibArtifact.isArtifactOnPlanet(locationId, artifactId), + "you can only activate artifacts you own or on planet" + ); + if (LibSpaceship.isShip(artifactId)) { + activateSpaceshipArtifact(locationId, artifactId, planet); + } else if (LibArtifact.isArtifact(artifactId)) { + Artifact memory artifact = LibArtifact.decode(artifactId); activateNonSpaceshipArtifact(locationId, artifactId, wormholeTo, planet, artifact); + } else { + require(false, "token cannot be activated"); } - - artifact.activations++; } function activateSpaceshipArtifact( uint256 locationId, - uint256 artifactId, - Planet storage planet, - Artifact storage artifact + uint256 shipId, + Planet storage planet ) private { - if (artifact.artifactType == ArtifactType.ShipCrescent) { - require(artifact.activations == 0, "crescent cannot be activated more than once"); - + Spaceship memory s = LibSpaceship.decode(shipId); + if (s.spaceshipType == SpaceshipType.ShipCrescent) { require( planet.planetType != PlanetType.SILVER_MINE, "cannot turn a silver mine into a silver mine" ); require(planet.owner == address(0), "can only activate crescent on unowned planets"); - require(planet.planetLevel >= 1, "planet level must be more than one"); - - artifact.lastActivated = block.timestamp; - artifact.lastDeactivated = block.timestamp; + require(planet.planetLevel >= 1, "planet level must be more than zero"); if (planet.silver == 0) { planet.silver = 1; @@ -184,7 +172,14 @@ library LibArtifactUtils { } planet.planetType = PlanetType.SILVER_MINE; - emit ArtifactActivated(msg.sender, artifactId, locationId); + emit ArtifactActivated(msg.sender, shipId, locationId); + + // TODO: Why not actually burn? + // burn it after use. will be owned by contract but not on a planet anyone can control + LibSpaceship.takeSpaceshipOffPlanet(locationId, shipId); + // BURN + DFTokenFacet(address(this)).burn(msg.sender, shipId, 1); + emit ArtifactDeactivated(msg.sender, shipId, locationId); } } @@ -200,39 +195,26 @@ library LibArtifactUtils { "you must own the planet you are activating an artifact on" ); require( - !LibGameUtils.getActiveArtifact(locationId).isInitialized, + !LibArtifact.hasActiveArtifact(locationId), "there is already an active artifact on this planet" ); require(!planet.destroyed, "planet is destroyed"); - require(artifact.isInitialized, "this artifact is not on this planet"); - - // Unknown is the 0th one, Monolith is the 1st, and so on. - // TODO v0.6: consider photoid canon - uint256[10] memory artifactCooldownsHours = [uint256(24), 0, 0, 0, 0, 4, 4, 24, 24, 24]; - - require( - artifact.lastDeactivated + - artifactCooldownsHours[uint256(artifact.artifactType)] * - 60 * - 60 < - block.timestamp, - "this artifact is on a cooldown" - ); - bool shouldDeactivateAndBurn = false; - artifact.lastActivated = block.timestamp; + gs().planets[locationId].artifactActivationTime = block.timestamp; + gs().planets[locationId].activeArtifact = artifactId; emit ArtifactActivated(msg.sender, artifactId, locationId); if (artifact.artifactType == ArtifactType.Wormhole) { require(wormholeTo != 0, "you must provide a wormholeTo to activate a wormhole"); + require( gs().planets[wormholeTo].owner == msg.sender, "you can only create a wormhole to a planet you own" ); require(!gs().planets[wormholeTo].destroyed, "planet destroyed"); - artifact.wormholeTo = wormholeTo; + gs().planets[locationId].wormholeTo = wormholeTo; } else if (artifact.artifactType == ArtifactType.BloomFilter) { require( 2 * uint256(artifact.rarity) >= planet.planetLevel, @@ -251,18 +233,30 @@ library LibArtifactUtils { } if (shouldDeactivateAndBurn) { - artifact.lastDeactivated = block.timestamp; // immediately deactivate - DFArtifactFacet(address(this)).updateArtifact(artifact); // save artifact state immediately, because _takeArtifactOffPlanet will access pull it from tokens contract + gs().planets[locationId].activeArtifact = 0; // immediately remove activate artifact + + LibGameUtils._buffPlanet(locationId, LibArtifact.getUpgradeForArtifact(artifact)); emit ArtifactDeactivated(msg.sender, artifactId, locationId); + DFTokenFacet(address(this)).burn(msg.sender, artifactId, 1); // burn it after use. will be owned by contract but not on a planet anyone can control - LibGameUtils._takeArtifactOffPlanet(artifactId, locationId); + // No need to take off, Artifact will never get placed. + // LibArtifact.takeArtifactOffPlanet(locationId, artifactId); } else { - DFArtifactFacet(address(this)).updateArtifact(artifact); + // this is fine even tho some artifacts are immediately deactivated, because + // those artifacts do not buff the planet. + LibArtifact.putArtifactOnPlanet(locationId, artifactId); + LibGameUtils._buffPlanet(locationId, LibArtifact.getUpgradeForArtifact(artifact)); + // Also transfer to contract + + // Only transfer if player is activating from inventory + if (DFTokenFacet(address(this)).tokenExists(msg.sender, artifactId)) { + DFArtifactFacet(address(this)).transferArtifact( + artifactId, + msg.sender, + gs().diamondAddress + ); + } } - - // this is fine even tho some artifacts are immediately deactivated, because - // those artifacts do not buff the planet. - LibGameUtils._buffPlanet(locationId, LibGameUtils._getUpgradeForArtifact(artifact)); } function deactivateArtifact(uint256 locationId) public { @@ -275,23 +269,29 @@ library LibArtifactUtils { require(!gs().planets[locationId].destroyed, "planet is destroyed"); - Artifact memory artifact = LibGameUtils.getActiveArtifact(locationId); + require( + LibArtifact.hasActiveArtifact(locationId), + "there is no artifact to deactivate on this planet" + ); - require(artifact.isInitialized, "this artifact is not activated on this planet"); + Artifact memory artifact = LibArtifact.getActiveArtifact(locationId); + + // In case just pretend there is a wormhole. + gs().planets[locationId].wormholeTo = 0; + gs().planets[locationId].activeArtifact = 0; + gs().planets[locationId].artifactActivationTime = 0; - artifact.lastDeactivated = block.timestamp; - artifact.wormholeTo = 0; emit ArtifactDeactivated(msg.sender, artifact.id, locationId); - DFArtifactFacet(address(this)).updateArtifact(artifact); bool shouldBurn = artifact.artifactType == ArtifactType.PlanetaryShield || artifact.artifactType == ArtifactType.PhotoidCannon; + if (shouldBurn) { // burn it after use. will be owned by contract but not on a planet anyone can control - LibGameUtils._takeArtifactOffPlanet(artifact.id, locationId); + LibArtifact.takeArtifactOffPlanet(locationId, artifact.id); } - LibGameUtils._debuffPlanet(locationId, LibGameUtils._getUpgradeForArtifact(artifact)); + LibGameUtils._debuffPlanet(locationId, LibArtifact.getUpgradeForArtifact(artifact)); } function depositArtifact( @@ -304,23 +304,26 @@ library LibArtifactUtils { require(!gs().planets[locationId].destroyed, "planet is destroyed"); require(planet.planetType == PlanetType.TRADING_POST, "can only deposit on trading posts"); require( - DFArtifactFacet(address(this)).ownerOf(artifactId) == msg.sender, + DFArtifactFacet(address(this)).tokenIsOwnedBy(msg.sender, artifactId), "you can only deposit artifacts you own" ); require(planet.owner == msg.sender, "you can only deposit on a planet you own"); - Artifact memory artifact = DFArtifactFacet(address(this)).getArtifact(artifactId); + Artifact memory artifact = LibArtifact.decode(artifactId); require( planet.planetLevel > uint256(artifact.rarity), "spacetime rip not high enough level to deposit this artifact" ); - require(!isSpaceship(artifact.artifactType), "cannot deposit spaceships"); - - require(gs().planetArtifacts[locationId].length < 5, "too many artifacts on this planet"); - LibGameUtils._putArtifactOnPlanet(artifactId, locationId); + require( + gs().planets[locationId].artifacts.length + gs().planets[locationId].spaceships.length < + 5, + "too many tokens on this planet" + ); - DFArtifactFacet(address(this)).transferArtifact(artifactId, coreAddress); + LibArtifact.putArtifactOnPlanet(locationId, artifactId); + // artifactId, curr owner, new owner + DFArtifactFacet(address(this)).transferArtifact(artifactId, msg.sender, coreAddress); } function withdrawArtifact(uint256 locationId, uint256 artifactId) public { @@ -332,17 +335,17 @@ library LibArtifactUtils { ); require(!gs().planets[locationId].destroyed, "planet is destroyed"); require(planet.owner == msg.sender, "you can only withdraw from a planet you own"); - Artifact memory artifact = LibGameUtils.getPlanetArtifact(locationId, artifactId); - require(artifact.isInitialized, "this artifact is not on this planet"); + Artifact memory artifact = LibArtifact.getPlanetArtifact(locationId, artifactId); require( planet.planetLevel > uint256(artifact.rarity), "spacetime rip not high enough level to withdraw this artifact" ); - require(!isSpaceship(artifact.artifactType), "cannot withdraw spaceships"); - LibGameUtils._takeArtifactOffPlanet(artifactId, locationId); - DFArtifactFacet(address(this)).transferArtifact(artifactId, msg.sender); + LibArtifact.takeArtifactOffPlanet(locationId, artifactId); + + // artifactId, curr owner, new owner + DFArtifactFacet(address(this)).transferArtifact(artifactId, address(this), msg.sender); } function prospectPlanet(uint256 locationId) public { @@ -361,12 +364,12 @@ library LibArtifactUtils { } function containsGear(uint256 locationId) public view returns (bool) { - uint256[] memory artifactIds = gs().planetArtifacts[locationId]; - - for (uint256 i = 0; i < artifactIds.length; i++) { - Artifact memory artifact = DFArtifactFacet(address(this)).getArtifact(artifactIds[i]); + uint256[] memory tokenIds = gs().planets[locationId].spaceships; + for (uint256 i = 0; i < tokenIds.length; i++) { + Spaceship memory spaceship = LibSpaceship.decode(tokenIds[i]); if ( - artifact.artifactType == ArtifactType.ShipGear && msg.sender == artifact.controller + spaceship.spaceshipType == SpaceshipType.ShipGear && + DFArtifactFacet(address(this)).tokenIsOwnedBy(msg.sender, tokenIds[i]) ) { return true; } @@ -374,9 +377,4 @@ library LibArtifactUtils { return false; } - - function isSpaceship(ArtifactType artifactType) public pure returns (bool) { - return - artifactType >= ArtifactType.ShipMothership && artifactType <= ArtifactType.ShipTitan; - } } diff --git a/eth/contracts/libraries/LibGameUtils.sol b/eth/contracts/libraries/LibGameUtils.sol index ded7d9d3..4bc35df0 100644 --- a/eth/contracts/libraries/LibGameUtils.sol +++ b/eth/contracts/libraries/LibGameUtils.sol @@ -6,11 +6,13 @@ import {DFArtifactFacet} from "../facets/DFArtifactFacet.sol"; // Library imports import {ABDKMath64x64} from "../vendor/libraries/ABDKMath64x64.sol"; +import {LibArtifact} from "./LibArtifact.sol"; +import {LibUtils} from "./LibUtils.sol"; // Storage imports import {LibStorage, GameStorage, GameConstants, SnarkConstants} from "./LibStorage.sol"; -import {Biome, SpaceType, Planet, PlanetType, PlanetEventType, Artifact, ArtifactType, ArtifactRarity, Upgrade, PlanetDefaultStats} from "../DFTypes.sol"; +import {Biome, SpaceType, Planet, PlanetType, PlanetEventType, Artifact, ArtifactType, TokenType, ArtifactRarity, Upgrade, PlanetDefaultStats} from "../DFTypes.sol"; library LibGameUtils { function gs() internal pure returns (GameStorage storage) { @@ -25,17 +27,6 @@ library LibGameUtils { return LibStorage.snarkConstants(); } - // inclusive on both ends - function _calculateByteUInt( - bytes memory _b, - uint256 _startByte, - uint256 _endByte - ) public pure returns (uint256 _byteUInt) { - for (uint256 i = _startByte; i <= _endByte; i++) { - _byteUInt += uint256(uint8(_b[i])) * (256**(_endByte - i)); - } - } - function _locationIdValid(uint256 _loc) public view returns (bool) { return (_loc < (21888242871839275222246405745257275088548364400416034343698204186575808495617 / @@ -87,7 +78,7 @@ library LibGameUtils { bytes memory _b = abi.encodePacked(_location); // get the uint value of byte 4 - 6 - uint256 _planetLevelUInt = _calculateByteUInt(_b, 4, 6); + uint256 _planetLevelUInt = LibUtils.calculateByteUInt(_b, 4, 6); uint256 level; // reverse-iterate thresholds and return planet type accordingly @@ -156,61 +147,6 @@ library LibGameUtils { } } - function _randomArtifactTypeAndLevelBonus( - uint256 artifactSeed, - Biome biome, - SpaceType spaceType - ) internal pure returns (ArtifactType, uint256) { - uint256 lastByteOfSeed = artifactSeed % 0xFF; - uint256 secondLastByteOfSeed = ((artifactSeed - lastByteOfSeed) / 256) % 0xFF; - - ArtifactType artifactType = ArtifactType.Pyramid; - - if (lastByteOfSeed < 39) { - artifactType = ArtifactType.Monolith; - } else if (lastByteOfSeed < 78) { - artifactType = ArtifactType.Colossus; - } - // else if (lastByteOfSeed < 117) { - // artifactType = ArtifactType.Spaceship; - // } - else if (lastByteOfSeed < 156) { - artifactType = ArtifactType.Pyramid; - } else if (lastByteOfSeed < 171) { - artifactType = ArtifactType.Wormhole; - } else if (lastByteOfSeed < 186) { - artifactType = ArtifactType.PlanetaryShield; - } else if (lastByteOfSeed < 201) { - artifactType = ArtifactType.PhotoidCannon; - } else if (lastByteOfSeed < 216) { - artifactType = ArtifactType.BloomFilter; - } else if (lastByteOfSeed < 231) { - artifactType = ArtifactType.BlackDomain; - } else { - if (biome == Biome.Ice) { - artifactType = ArtifactType.PlanetaryShield; - } else if (biome == Biome.Lava) { - artifactType = ArtifactType.PhotoidCannon; - } else if (biome == Biome.Wasteland) { - artifactType = ArtifactType.BloomFilter; - } else if (biome == Biome.Corrupted) { - artifactType = ArtifactType.BlackDomain; - } else { - artifactType = ArtifactType.Wormhole; - } - artifactType = ArtifactType.PhotoidCannon; - } - - uint256 bonus = 0; - if (secondLastByteOfSeed < 4) { - bonus = 2; - } else if (secondLastByteOfSeed < 16) { - bonus = 1; - } - - return (artifactType, bonus); - } - // TODO v0.6: handle corrupted biomes function _getBiome(SpaceType spaceType, uint256 biomebase) public view returns (Biome) { if (spaceType == SpaceType.DEAD_SPACE) { @@ -267,212 +203,6 @@ library LibGameUtils { }); } - function _getUpgradeForArtifact(Artifact memory artifact) public pure returns (Upgrade memory) { - if (artifact.artifactType == ArtifactType.PlanetaryShield) { - uint256[6] memory defenseMultipliersPerRarity = [uint256(100), 150, 200, 300, 450, 650]; - - return - Upgrade({ - popCapMultiplier: 100, - popGroMultiplier: 100, - rangeMultiplier: 20, - speedMultiplier: 20, - defMultiplier: defenseMultipliersPerRarity[uint256(artifact.rarity)] - }); - } - - if (artifact.artifactType == ArtifactType.PhotoidCannon) { - uint256[6] memory def = [uint256(100), 50, 40, 30, 20, 10]; - return - Upgrade({ - popCapMultiplier: 100, - popGroMultiplier: 100, - rangeMultiplier: 100, - speedMultiplier: 100, - defMultiplier: def[uint256(artifact.rarity)] - }); - } - - if (uint256(artifact.artifactType) >= 5) { - return - Upgrade({ - popCapMultiplier: 100, - popGroMultiplier: 100, - rangeMultiplier: 100, - speedMultiplier: 100, - defMultiplier: 100 - }); - } - - Upgrade memory ret = Upgrade({ - popCapMultiplier: 100, - popGroMultiplier: 100, - rangeMultiplier: 100, - speedMultiplier: 100, - defMultiplier: 100 - }); - - if (artifact.artifactType == ArtifactType.Monolith) { - ret.popCapMultiplier += 5; - ret.popGroMultiplier += 5; - } else if (artifact.artifactType == ArtifactType.Colossus) { - ret.speedMultiplier += 5; - } else if (artifact.artifactType == ArtifactType.Spaceship) { - ret.rangeMultiplier += 5; - } else if (artifact.artifactType == ArtifactType.Pyramid) { - ret.defMultiplier += 5; - } - - if (artifact.planetBiome == Biome.Ocean) { - ret.speedMultiplier += 5; - ret.defMultiplier += 5; - } else if (artifact.planetBiome == Biome.Forest) { - ret.defMultiplier += 5; - ret.popCapMultiplier += 5; - ret.popGroMultiplier += 5; - } else if (artifact.planetBiome == Biome.Grassland) { - ret.popCapMultiplier += 5; - ret.popGroMultiplier += 5; - ret.rangeMultiplier += 5; - } else if (artifact.planetBiome == Biome.Tundra) { - ret.defMultiplier += 5; - ret.rangeMultiplier += 5; - } else if (artifact.planetBiome == Biome.Swamp) { - ret.speedMultiplier += 5; - ret.rangeMultiplier += 5; - } else if (artifact.planetBiome == Biome.Desert) { - ret.speedMultiplier += 10; - } else if (artifact.planetBiome == Biome.Ice) { - ret.rangeMultiplier += 10; - } else if (artifact.planetBiome == Biome.Wasteland) { - ret.defMultiplier += 10; - } else if (artifact.planetBiome == Biome.Lava) { - ret.popCapMultiplier += 10; - ret.popGroMultiplier += 10; - } else if (artifact.planetBiome == Biome.Corrupted) { - ret.rangeMultiplier += 5; - ret.speedMultiplier += 5; - ret.popCapMultiplier += 5; - ret.popGroMultiplier += 5; - } - - uint256 scale = 1 + (uint256(artifact.rarity) / 2); - - ret.popCapMultiplier = scale * ret.popCapMultiplier - (scale - 1) * 100; - ret.popGroMultiplier = scale * ret.popGroMultiplier - (scale - 1) * 100; - ret.speedMultiplier = scale * ret.speedMultiplier - (scale - 1) * 100; - ret.rangeMultiplier = scale * ret.rangeMultiplier - (scale - 1) * 100; - ret.defMultiplier = scale * ret.defMultiplier - (scale - 1) * 100; - - return ret; - } - - function artifactRarityFromPlanetLevel(uint256 planetLevel) - public - pure - returns (ArtifactRarity) - { - if (planetLevel <= 1) return ArtifactRarity.Common; - else if (planetLevel <= 3) return ArtifactRarity.Rare; - else if (planetLevel <= 5) return ArtifactRarity.Epic; - else if (planetLevel <= 7) return ArtifactRarity.Legendary; - else return ArtifactRarity.Mythic; - } - - // planets can have multiple artifacts on them. this function updates all the - // internal contract book-keeping to reflect that the given artifact was - // put on. note that this function does not transfer the artifact. - function _putArtifactOnPlanet(uint256 artifactId, uint256 locationId) public { - gs().artifactIdToPlanetId[artifactId] = locationId; - gs().planetArtifacts[locationId].push(artifactId); - } - - // planets can have multiple artifacts on them. this function updates all the - // internal contract book-keeping to reflect that the given artifact was - // taken off the given planet. note that this function does not transfer the - // artifact. - // - // if the given artifact is not on the given planet, reverts - // if the given artifact is currently activated, reverts - function _takeArtifactOffPlanet(uint256 artifactId, uint256 locationId) public { - uint256 artifactsOnThisPlanet = gs().planetArtifacts[locationId].length; - bool hadTheArtifact = false; - - for (uint256 i = 0; i < artifactsOnThisPlanet; i++) { - if (gs().planetArtifacts[locationId][i] == artifactId) { - Artifact memory artifact = DFArtifactFacet(address(this)).getArtifact( - gs().planetArtifacts[locationId][i] - ); - - require( - !isActivated(artifact), - "you cannot take an activated artifact off a planet" - ); - - gs().planetArtifacts[locationId][i] = gs().planetArtifacts[locationId][ - artifactsOnThisPlanet - 1 - ]; - - hadTheArtifact = true; - break; - } - } - - require(hadTheArtifact, "this artifact was not present on this planet"); - gs().artifactIdToPlanetId[artifactId] = 0; - gs().planetArtifacts[locationId].pop(); - } - - // an artifact is only considered 'activated' if this method returns true. - // we do not have an `isActive` field on artifact; the times that the - // artifact was last activated and deactivated are sufficent to determine - // whether or not the artifact is activated. - function isActivated(Artifact memory artifact) public pure returns (bool) { - return artifact.lastDeactivated < artifact.lastActivated; - } - - function isArtifactOnPlanet(uint256 locationId, uint256 artifactId) public returns (bool) { - for (uint256 i; i < gs().planetArtifacts[locationId].length; i++) { - if (gs().planetArtifacts[locationId][i] == artifactId) { - return true; - } - } - - return false; - } - - // if the given artifact is on the given planet, then return the artifact - // otherwise, return a 'null' artifact - function getPlanetArtifact(uint256 locationId, uint256 artifactId) - public - view - returns (Artifact memory) - { - for (uint256 i; i < gs().planetArtifacts[locationId].length; i++) { - if (gs().planetArtifacts[locationId][i] == artifactId) { - return DFArtifactFacet(address(this)).getArtifact(artifactId); - } - } - - return _nullArtifact(); - } - - // if the given planet has an activated artifact on it, then return the artifact - // otherwise, return a 'null artifact' - function getActiveArtifact(uint256 locationId) public view returns (Artifact memory) { - for (uint256 i; i < gs().planetArtifacts[locationId].length; i++) { - Artifact memory artifact = DFArtifactFacet(address(this)).getArtifact( - gs().planetArtifacts[locationId][i] - ); - - if (isActivated(artifact)) { - return artifact; - } - } - - return _nullArtifact(); - } - // the space junk that a planet starts with function getPlanetDefaultSpaceJunk(Planet memory planet) public view returns (uint256) { if (planet.isHomePlanet) return 0; @@ -480,27 +210,6 @@ library LibGameUtils { return gameConstants().PLANET_LEVEL_JUNK[planet.planetLevel]; } - // constructs a new artifact whose `isInititalized` field is set to `false` - // used to represent the concept of 'no artifact' - function _nullArtifact() private pure returns (Artifact memory) { - return - Artifact( - false, - 0, - 0, - ArtifactRarity(0), - Biome(0), - 0, - address(0), - ArtifactType(0), - 0, - 0, - 0, - 0, - address(0) - ); - } - function _buffPlanet(uint256 location, Upgrade memory upgrade) public { Planet storage planet = gs().planets[location]; diff --git a/eth/contracts/libraries/LibLazyUpdate.sol b/eth/contracts/libraries/LibLazyUpdate.sol index 0c3944f3..fa3daaaf 100644 --- a/eth/contracts/libraries/LibLazyUpdate.sol +++ b/eth/contracts/libraries/LibLazyUpdate.sol @@ -8,7 +8,7 @@ import {ABDKMath64x64} from "../vendor/libraries/ABDKMath64x64.sol"; import {LibStorage, GameStorage} from "./LibStorage.sol"; // Type imports -import {Planet, PlanetType, PlanetEventMetadata, PlanetEventType, ArrivalData, ArrivalType, Artifact} from "../DFTypes.sol"; +import {Planet, PlanetType, PlanetEventMetadata, PlanetEventType, ArrivalData, ArrivalType} from "../DFTypes.sol"; library LibLazyUpdate { function gs() internal pure returns (GameStorage storage) { diff --git a/eth/contracts/libraries/LibPlanet.sol b/eth/contracts/libraries/LibPlanet.sol index acf3c307..50fda7a7 100644 --- a/eth/contracts/libraries/LibPlanet.sol +++ b/eth/contracts/libraries/LibPlanet.sol @@ -3,16 +3,21 @@ pragma solidity ^0.8.0; // Contract imports import {DFVerifierFacet} from "../facets/DFVerifierFacet.sol"; +import {DFTokenFacet} from "../facets/DFTokenFacet.sol"; // Library imports +import {LibArtifact} from "./LibArtifact.sol"; +import {LibArtifactUtils} from "./LibArtifactUtils.sol"; import {LibGameUtils} from "./LibGameUtils.sol"; import {LibLazyUpdate} from "./LibLazyUpdate.sol"; +import {LibSilver} from "./LibSilver.sol"; +import {LibSpaceship} from "./LibSpaceship.sol"; // Storage imports import {LibStorage, GameStorage, GameConstants, SnarkConstants} from "./LibStorage.sol"; // Type imports -import {Artifact, ArtifactType, DFPInitPlanetArgs, Planet, PlanetEventMetadata, PlanetType, RevealedCoords, SpaceType, Upgrade, UpgradeBranch} from "../DFTypes.sol"; +import {ArtifactType, Artifact, DFPInitPlanetArgs, Planet, PlanetEventMetadata, PlanetType, RevealedCoords, SpaceType, Spaceship, SpaceshipType, Upgrade, UpgradeBranch} from "../DFTypes.sol"; library LibPlanet { function gs() internal pure returns (GameStorage storage) { @@ -28,8 +33,6 @@ library LibPlanet { } // also need to copy some of DFCore's event signatures - event ArtifactActivated(address player, uint256 artifactId, uint256 loc); - event ArtifactDeactivated(address player, uint256 artifactId, uint256 loc); event PlanetUpgraded(address player, uint256 loc, uint256 branch, uint256 toBranchLevel); function revealLocation( @@ -159,6 +162,12 @@ library LibPlanet { _planet.planetLevel = defaultPlanet.planetLevel; _planet.planetType = defaultPlanet.planetType; + _planet.artifacts = defaultPlanet.artifacts; + _planet.spaceships = defaultPlanet.spaceships; + _planet.activeArtifact = defaultPlanet.activeArtifact; + _planet.wormholeTo = defaultPlanet.wormholeTo; + _planet.artifactActivationTime = defaultPlanet.artifactActivationTime; + _planet.isInitialized = true; _planet.perlin = args.perlin; _planet.spaceType = args.spaceType; @@ -285,9 +294,11 @@ library LibPlanet { } for (uint256 i = 0; i < artifactsToAdd.length; i++) { - Artifact memory artifact = gs().artifacts[artifactsToAdd[i]]; - - planet = applySpaceshipArrive(artifact, planet); + // Only apply Spaceship arrival if ship is a spaceship. + if (LibSpaceship.isShip(artifactsToAdd[i])) { + Spaceship memory spaceship = LibSpaceship.decode(artifactsToAdd[i]); + planet = applySpaceshipArrive(spaceship, planet); + } } planet = LibLazyUpdate.updatePlanet(timestamp, planet); @@ -295,7 +306,7 @@ library LibPlanet { return (planet, eventsToRemove, artifactsToAdd); } - function applySpaceshipArrive(Artifact memory artifact, Planet memory planet) + function applySpaceshipArrive(Spaceship memory spaceship, Planet memory planet) public pure returns (Planet memory) @@ -304,17 +315,17 @@ library LibPlanet { return planet; } - if (artifact.artifactType == ArtifactType.ShipMothership) { + if (spaceship.spaceshipType == SpaceshipType.ShipMothership) { if (planet.energyGroDoublers == 0) { planet.populationGrowth *= 2; } planet.energyGroDoublers++; - } else if (artifact.artifactType == ArtifactType.ShipWhale) { + } else if (spaceship.spaceshipType == SpaceshipType.ShipWhale) { if (planet.silverGroDoublers == 0) { planet.silverGrowth *= 2; } planet.silverGroDoublers++; - } else if (artifact.artifactType == ArtifactType.ShipTitan) { + } else if (spaceship.spaceshipType == SpaceshipType.ShipTitan) { planet.pausers++; } @@ -327,7 +338,7 @@ library LibPlanet { ( Planet memory planet, uint256[12] memory eventsToRemove, - uint256[12] memory artifactIdsToAddToPlanet + uint256[12] memory tokenIdsToAddToPlanet ) = getRefreshedPlanet(location, block.timestamp); gs().planets[location] = planet; @@ -344,9 +355,12 @@ library LibPlanet { } for (uint256 i = 0; i < 12; i++) { - if (artifactIdsToAddToPlanet[i] != 0) { - gs().artifactIdToVoyageId[artifactIdsToAddToPlanet[i]] = 0; - LibGameUtils._putArtifactOnPlanet(artifactIdsToAddToPlanet[i], location); + if (tokenIdsToAddToPlanet[i] != 0) { + if (LibSpaceship.isShip(tokenIdsToAddToPlanet[i])) { + LibSpaceship.putSpaceshipOnPlanet(location, tokenIdsToAddToPlanet[i]); + } else if (LibArtifact.isArtifact(tokenIdsToAddToPlanet[i])) { + LibArtifact.putArtifactOnPlanet(location, tokenIdsToAddToPlanet[i]); + } } } } @@ -370,6 +384,10 @@ library LibPlanet { // so any of those values coming from the contracts need to be divided by // `CONTRACT_PRECISION` to get their true integer value. uint256 scoreGained = silverToWithdraw / 1000; + // increase silver token count; + uint256 silverId = LibSilver.create(); + DFTokenFacet(address(this)).mint(msg.sender, silverId, scoreGained); + scoreGained = (scoreGained * gameConstants().SILVER_SCORE_VALUE) / 100; gs().players[msg.sender].score += scoreGained; } diff --git a/eth/contracts/libraries/LibSilver.sol b/eth/contracts/libraries/LibSilver.sol new file mode 100644 index 00000000..82c1d3fb --- /dev/null +++ b/eth/contracts/libraries/LibSilver.sol @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.0; + +/** + * Library for all things Silver + */ + +// Contract imports +import "hardhat/console.sol"; + +// Library imports +import {LibUtils} from "./LibUtils.sol"; + +// Type imports +import {SilverInfo, TokenType} from "../DFTypes.sol"; + +library LibSilver { + /** + * @notice Create the token ID for Silver. + * Takes no args because silver is a fungible resource. + */ + function create() internal pure returns (uint256) { + // x << y is equivalent to the mathematical expression x * 2**y + uint256 tokenType = LibUtils.shiftLeft( + uint8(TokenType.Silver), + uint8(SilverInfo.TokenType) + ); + return tokenType; + } + + function decode(uint256 silverId) internal pure returns (uint256) { + bytes memory _b = abi.encodePacked(silverId); + // Idx is subtracted by one because each Info enum has Unknown at the zero location. + uint8 tokenIdx = uint8(SilverInfo.TokenType) - 1; + + uint8 tokenType = uint8(LibUtils.calculateByteUInt(_b, tokenIdx, tokenIdx)); + + require(tokenType == uint8(TokenType.Silver), "token is not silver"); + return silverId; + } + + function isSilver(uint256 tokenId) internal pure returns (bool) { + bytes memory _b = abi.encodePacked(tokenId); + uint8 tokenIdx = uint8(SilverInfo.TokenType) - 1; + uint8 tokenType = uint8(LibUtils.calculateByteUInt(_b, tokenIdx, tokenIdx)); + return (tokenType == uint8(TokenType.Silver)); + } +} diff --git a/eth/contracts/libraries/LibSpaceship.sol b/eth/contracts/libraries/LibSpaceship.sol new file mode 100644 index 00000000..0cff4526 --- /dev/null +++ b/eth/contracts/libraries/LibSpaceship.sol @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.0; + +/** + * Library for all things Spaceships + */ + +// Library imports +import {LibUtils} from "./LibUtils.sol"; + +// Storage imports +import {LibStorage, GameStorage, GameConstants, SnarkConstants} from "./LibStorage.sol"; + +// Type imports +import {Spaceship, SpaceshipInfo, SpaceshipType, TokenType} from "../DFTypes.sol"; + +library LibSpaceship { + function gs() internal pure returns (GameStorage storage) { + return LibStorage.gameStorage(); + } + + /** + * @notice Create the token ID for a Spaceship with the following properties: + * @param spaceshipType SpaceshipType. + */ + function create(SpaceshipType spaceshipType) internal pure returns (uint256) { + require(isValidShipType(spaceshipType), "spaceship type is not valid"); + + uint256 tokenType = LibUtils.shiftLeft( + uint8(TokenType.Spaceship), + uint8(SpaceshipInfo.TokenType) + ); + uint256 shipType = LibUtils.shiftLeft( + uint8(spaceshipType), + uint8(SpaceshipInfo.SpaceshipType) + ); + return tokenType + shipType; + } + + function decode(uint256 spaceshipId) internal pure returns (Spaceship memory) { + bytes memory _b = abi.encodePacked(spaceshipId); + // Idx is subtracted by one because each Info enum has Unknown at the zero location. + uint8 tokenIdx = uint8(SpaceshipInfo.TokenType) - 1; + uint8 shipInfoIdx = uint8(SpaceshipInfo.SpaceshipType) - 1; + + uint8 tokenType = uint8(LibUtils.calculateByteUInt(_b, tokenIdx, tokenIdx)); + uint8 shipType = uint8(LibUtils.calculateByteUInt(_b, shipInfoIdx, shipInfoIdx)); + + require(isShip(spaceshipId), "token type is not spaceship"); + require(isValidShipType(SpaceshipType(shipType)), "spaceship type is not valid"); + + return + Spaceship({ + id: spaceshipId, + tokenType: TokenType(tokenType), + spaceshipType: SpaceshipType(shipType) + }); + } + + function isShip(uint256 tokenId) internal pure returns (bool) { + bytes memory _b = abi.encodePacked(tokenId); + uint8 tokenIdx = uint8(SpaceshipInfo.TokenType) - 1; + uint8 tokenType = uint8(LibUtils.calculateByteUInt(_b, tokenIdx, tokenIdx)); + return (tokenType == uint8(TokenType.Spaceship)); + } + + function isValidShipType(SpaceshipType shipType) internal pure returns (bool) { + return (shipType >= SpaceshipType.ShipMothership && shipType <= SpaceshipType.ShipTitan); + } + + function isSpaceshipOnPlanet(uint256 locationId, uint256 shipId) internal view returns (bool) { + for (uint256 i; i < gs().planets[locationId].spaceships.length; i++) { + if (gs().planets[locationId].spaceships[i] == shipId) { + return true; + } + } + return false; + } + + function putSpaceshipOnPlanet(uint256 locationId, uint256 spaceshipId) internal { + gs().planets[locationId].spaceships.push(spaceshipId); + } + + function takeSpaceshipOffPlanet(uint256 locationId, uint256 spaceshipId) internal { + uint256 shipsOnThisPlanet = gs().planets[locationId].spaceships.length; + + bool hadTheShip = false; + + for (uint256 i = 0; i < shipsOnThisPlanet; i++) { + if (gs().planets[locationId].spaceships[i] == spaceshipId) { + gs().planets[locationId].spaceships[i] = gs().planets[locationId].spaceships[ + shipsOnThisPlanet - 1 + ]; + + hadTheShip = true; + break; + } + } + + require(hadTheShip, "this ship was not present on this planet"); + gs().planets[locationId].spaceships.pop(); + } +} diff --git a/eth/contracts/libraries/LibStorage.sol b/eth/contracts/libraries/LibStorage.sol index fb375c08..1c521033 100644 --- a/eth/contracts/libraries/LibStorage.sol +++ b/eth/contracts/libraries/LibStorage.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.0; // Type imports -import {Planet, PlanetEventMetadata, PlanetDefaultStats, Upgrade, RevealedCoords, Player, ArrivalData, Artifact} from "../DFTypes.sol"; +import {Planet, PlanetEventMetadata, PlanetDefaultStats, Upgrade, RevealedCoords, Player, ArrivalData} from "../DFTypes.sol"; struct WhitelistStorage { bool enabled; @@ -38,16 +38,11 @@ struct GameStorage { uint256 miscNonce; mapping(uint256 => Planet) planets; mapping(uint256 => RevealedCoords) revealedCoords; - mapping(uint256 => uint256) artifactIdToPlanetId; - mapping(uint256 => uint256) artifactIdToVoyageId; mapping(address => Player) players; // maps location id to planet events array mapping(uint256 => PlanetEventMetadata[]) planetEvents; // maps event id to arrival data mapping(uint256 => ArrivalData) planetArrivals; - mapping(uint256 => uint256[]) planetArtifacts; - // Artifact stuff - mapping(uint256 => Artifact) artifacts; // Capture Zones uint256 nextChangeBlock; } diff --git a/eth/contracts/libraries/LibUtils.sol b/eth/contracts/libraries/LibUtils.sol new file mode 100644 index 00000000..7363a683 --- /dev/null +++ b/eth/contracts/libraries/LibUtils.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.0; + +/** + * General Purpose Utilities. Must be pure functions. + */ + +library LibUtils { + /** + * @notice calculate amount of bits to shift left + * @param index number of 1 byte words to shift from left + * @return shift length of left shift + */ + function calcBitShift(uint8 index) internal pure returns (uint8) { + uint8 maxVal = 32; + + require(index <= maxVal, "shift index is too high"); + require(index > 0, "shift index is too low"); + + uint256 bin = 8; + uint256 shift = 256; + return uint8(shift - (bin * index)); + } + + // inclusive on both ends + function calculateByteUInt( + bytes memory _b, + uint256 _startByte, + uint256 _endByte + ) internal pure returns (uint256 _byteUInt) { + for (uint256 i = _startByte; i <= _endByte; i++) { + _byteUInt += uint256(uint8(_b[i])) * (256**(_endByte - i)); + } + } + + /** + * @notice x << y where y = 2^calcBitShift(index)) + */ + function shiftLeft(uint8 value, uint8 index) internal pure returns (uint256) { + return (value * 2**calcBitShift(index)); + } +} diff --git a/eth/hardhat.config.ts b/eth/hardhat.config.ts index fc7dee6e..fa307e48 100644 --- a/eth/hardhat.config.ts +++ b/eth/hardhat.config.ts @@ -27,7 +27,6 @@ import { Initializers, } from '@dfdao/settings'; import { workspace } from '@projectsophon/workspace'; -import './tasks/artifact'; import './tasks/circom'; import './tasks/debug'; import './tasks/deploy'; diff --git a/eth/package.json b/eth/package.json index de7bb054..3f2c2f4f 100644 --- a/eth/package.json +++ b/eth/package.json @@ -63,6 +63,7 @@ "node": ">=16" }, "scripts": { + "hardhat": "hardhat", "test": "hardhat test && npm run subgraph:template:dev", "lint": "eslint .", "format": "prettier --write .", diff --git a/eth/tasks/artifact.ts b/eth/tasks/artifact.ts deleted file mode 100644 index fd74c6db..00000000 --- a/eth/tasks/artifact.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { task } from 'hardhat/config'; -import { HardhatRuntimeEnvironment } from 'hardhat/types'; - -task('artifact:read', 'Read Artifact data from Tokens contract').setAction(artifactsRead); - -async function artifactsRead({}, hre: HardhatRuntimeEnvironment) { - const contract = await hre.ethers.getContractAt( - 'DarkForest', - hre.settings.contracts.CONTRACT_ADDRESS - ); - - const id = await contract.tokenByIndex(0); - console.log(id.toString()); - const token = await contract.getArtifact(id); - console.log(token); - const URI = await contract.tokenURI(id); - console.log(URI); -} diff --git a/eth/tasks/debug.ts b/eth/tasks/debug.ts index 66ac78b1..1e1eed53 100644 --- a/eth/tasks/debug.ts +++ b/eth/tasks/debug.ts @@ -1,122 +1,118 @@ // these tasks are intended to simplify the debugging and development workflow by essentially giving -// you god-mode status, allowing you to change the state of the world however you want. - -import { task, types } from 'hardhat/config'; -import { HardhatRuntimeEnvironment } from 'hardhat/types'; // see DFTypes.sol - ArtifactType -const artifactOptions = ( - 'Monolith,Colossus,Spaceship,Pyramid,Wormhole,' + - 'PlanetaryShield,PhotoidCannon,BloomFilter,BlackDomain' -).split(','); +// const artifactOptions = ( +// 'Monolith,Colossus,Spaceship,Pyramid,Wormhole,' + +// 'PlanetaryShield,PhotoidCannon,BloomFilter,BlackDomain' +// ).split(','); -// npm run --workspace eth hardhat:dev -- debug:giveArtifact "0x27fd6eec1e1f3ce4a53b40d5813119d868f7b4e3" PhotoidCannon 5 -task('debug:giveArtifact', 'gives the player some amount of a particular type of artifact') - .addPositionalParam( - 'playerAddress', - 'the address of the player to give the artifacts', - undefined, - types.string - ) - .addPositionalParam( - 'artifactType', - 'one of: [Monolith, Colossus, Spaceship, Pyramid, Wormhole, ' + - 'PlanetaryShield, PhotoidCannon, BloomFilter, BlackDomain]', - undefined, - types.string - ) - .addPositionalParam('amount', 'the amount of this artifact to give', 1, types.int) - .addPositionalParam('rarity', 'the rarity of the artifact to give', 1, types.int) - .addPositionalParam('biome', 'the biome of the artifact to give', 1, types.int) - .addPositionalParam( - 'discoveredOn', - 'the planet ID (decimal string) this was discovered on', - '0', - types.string - ) - .setAction(giveArtifact); +// // npm run --workspace eth hardhat:dev -- debug:giveArtifact "0x27fd6eec1e1f3ce4a53b40d5813119d868f7b4e3" PhotoidCannon 5 +// task('debug:giveArtifact', 'gives the player some amount of a particular type of artifact') +// .addPositionalParam( +// 'playerAddress', +// 'the address of the player to give the artifacts', +// undefined, +// types.string +// ) +// .addPositionalParam( +// 'artifactType', +// 'one of: [Monolith, Colossus, Spaceship, Pyramid, Wormhole, ' + +// 'PlanetaryShield, PhotoidCannon, BloomFilter, BlackDomain]', +// undefined, +// types.string +// ) +// .addPositionalParam('amount', 'the amount of this artifact to give', 1, types.int) +// .addPositionalParam('rarity', 'the rarity of the artifact to give', 1, types.int) +// .addPositionalParam('biome', 'the biome of the artifact to give', 1, types.int) +// .addPositionalParam( +// 'discoveredOn', +// 'the planet ID (decimal string) this was discovered on', +// '0', +// types.string +// ) +// .setAction(giveArtifact); -async function giveArtifact( - { - playerAddress, - artifactType, - amount, - rarity, - biome, - discoveredOn, - }: { - playerAddress: string; - artifactType: string; - amount: number; - rarity: number; - biome: number; - discoveredOn: string; - }, - hre: HardhatRuntimeEnvironment -) { - const chosenArtifactType = artifactOptions.indexOf(artifactType) + 1; - const contract = await hre.ethers.getContractAt( - 'DarkForest', - hre.settings.contracts.CONTRACT_ADDRESS - ); +// async function giveArtifact( +// { +// playerAddress, +// artifactType, +// amount, +// rarity, +// biome, +// discoveredOn, +// }: { +// playerAddress: string; +// artifactType: string; +// amount: number; +// rarity: number; +// biome: number; +// discoveredOn: string; +// }, +// hre: HardhatRuntimeEnvironment +// ) { +// const chosenArtifactType = artifactOptions.indexOf(artifactType) + 1; +// const contract = await hre.ethers.getContractAt( +// 'DarkForest', +// hre.settings.contracts.CONTRACT_ADDRESS +// ); - for (let i = 0; i < amount; i++) { - // see contracts/types/ActionTypes.sol - CreateArtifactArgs - const createArtifactArgs = { - tokenId: random256Id(), - discoverer: playerAddress, - planetId: discoveredOn, - rarity: rarity, - biome: biome, - artifactType: chosenArtifactType, - owner: playerAddress, - controller: '0x0000000000000000000000000000000000000000', - }; +// for (let i = 0; i < amount; i++) { +// // see contracts/types/ActionTypes.sol - CreateArtifactArgs +// const createArtifactArgs = { +// tokenId: random256Id(), +// discoverer: playerAddress, +// planetId: discoveredOn, +// rarity: rarity, +// biome: biome, +// artifactType: chosenArtifactType, +// owner: playerAddress, +// controller: '0x0000000000000000000000000000000000000000', +// }; - await (await contract.createArtifact(createArtifactArgs)).wait(); - } -} +// await (await contract.createArtifact(createArtifactArgs)).wait(); +// } +// } // npm run --workspace eth hardhat:dev -- debug:giveOneOfEachArtifact "0x5bcf0ac4c057dcaf9b23e4dd7cb7b035a71dd0dc" 10 -task( - 'debug:giveOneOfEachArtifact', - 'gives the player one of each type of artifact, one of each rarity' -) - .addPositionalParam( - 'playerAddress', - 'the address of the player to give the artifacts', - undefined, - types.string - ) - .addPositionalParam('biome', 'the biome of the artifacts to give', 1, types.int) - .setAction(giveOneOfEachArtifact); +// task( +// 'debug:giveOneOfEachArtifact', +// 'gives the player one of each type of artifact, one of each rarity' +// ) +// .addPositionalParam( +// 'playerAddress', +// 'the address of the player to give the artifacts', +// undefined, +// types.string +// ) +// .addPositionalParam('biome', 'the biome of the artifacts to give', 1, types.int) +// .setAction(giveOneOfEachArtifact); -async function giveOneOfEachArtifact( - { playerAddress, biome }: { playerAddress: string; biome: number }, - hre: HardhatRuntimeEnvironment -) { - for (const artifact of artifactOptions) { - for (let i = 1; i < 6; i++) { - await giveArtifact( - { - playerAddress, - artifactType: artifact, - amount: 1, - rarity: i, - biome, - discoveredOn: '0', - }, - hre - ); - } - } -} +// async function giveOneOfEachArtifact( +// { playerAddress, biome }: { playerAddress: string; biome: number }, +// hre: HardhatRuntimeEnvironment +// ) { +// for (const artifact of artifactOptions) { +// for (let i = 1; i < 6; i++) { +// await giveArtifact( +// { +// playerAddress, +// artifactType: artifact, +// amount: 1, +// rarity: i, +// biome, +// discoveredOn: '0', +// }, +// hre +// ); +// } +// } +// } -function random256Id() { - const alphabet = '0123456789ABCDEF'.split(''); - let result = '0x'; - for (let i = 0; i < 256 / 4; i++) { - result += alphabet[Math.floor(Math.random() * alphabet.length)]; - } - return result; -} +// function random256Id() { +// const alphabet = '0123456789ABCDEF'.split(''); +// let result = '0x'; +// for (let i = 0; i < 256 / 4; i++) { +// result += alphabet[Math.floor(Math.random() * alphabet.length)]; +// } +// return result; +// } diff --git a/eth/tasks/deploy.ts b/eth/tasks/deploy.ts index e7486cc1..59d95838 100644 --- a/eth/tasks/deploy.ts +++ b/eth/tasks/deploy.ts @@ -246,6 +246,7 @@ export async function deployAndCut( const coreFacet = await deployCoreFacet({}, libraries, hre); const moveFacet = await deployMoveFacet({}, libraries, hre); const captureFacet = await deployCaptureFacet({}, libraries, hre); + const tokenFacet = await deployTokenFacet({}, libraries, hre); const artifactFacet = await deployArtifactFacet( { diamondAddress: diamond.address }, libraries, @@ -270,6 +271,7 @@ export async function deployAndCut( ...changes.getFacetCuts('DFAdminFacet', adminFacet), ...changes.getFacetCuts('DFLobbyFacet', lobbyFacet), ...changes.getFacetCuts('DFRewardFacet', rewardFacet), + ...changes.getFacetCuts('DFTokenFacet', tokenFacet), ]; if (isDev) { @@ -309,15 +311,9 @@ export async function deployAndCut( return [diamond, diamondInit, initReceipt] as const; } -export async function deployGetterFacet( - {}, - { LibGameUtils }: Libraries, - hre: HardhatRuntimeEnvironment -) { +export async function deployGetterFacet({}, {}: Libraries, hre: HardhatRuntimeEnvironment) { const factory = await hre.ethers.getContractFactory('DFGetterFacet', { - libraries: { - LibGameUtils, - }, + libraries: {}, }); const contract = await factory.deploy(); await contract.deployTransaction.wait(); @@ -375,6 +371,16 @@ export async function deployVerifierFacet({}, {}: Libraries, hre: HardhatRuntime return contract; } +export async function deployTokenFacet({}, {}: Libraries, hre: HardhatRuntimeEnvironment) { + const factory = await hre.ethers.getContractFactory('DFTokenFacet', { + libraries: {}, + }); + const contract = await factory.deploy(); + await contract.deployTransaction.wait(); + console.log(`DFTokenFacet deployed to: ${contract.address}`); + return contract; +} + export async function deployArtifactFacet( {}, { LibGameUtils, LibPlanet, LibArtifactUtils }: Libraries, diff --git a/eth/test/DFArtifacts.test.ts b/eth/test/DFArtifacts.test.ts index b07bd8d8..4759baf2 100644 --- a/eth/test/DFArtifacts.test.ts +++ b/eth/test/DFArtifacts.test.ts @@ -1,37 +1,37 @@ -import { ArtifactRarity, ArtifactType, Biome } from '@dfdao/types'; -import { loadFixture } from '@nomicfoundation/hardhat-network-helpers'; +import { ArtifactRarity, ArtifactType, Biome, SpaceshipType, TokenType } from '@dfdao/types'; +import { loadFixture, mine } from '@nomicfoundation/hardhat-network-helpers'; import { expect } from 'chai'; -import { BigNumberish } from 'ethers'; import hre from 'hardhat'; -import { TestLocation } from './utils/TestLocation'; import { + activateAndConfirm, conquerUnownedPlanet, - createArtifactOnPlanet, - getArtifactsOwnedBy, + createArtifact, + getArtifactsOnPlanet, getCurrentTime, getStatSum, - hexToBigNumber, increaseBlockchainTime, makeFindArtifactArgs, makeInitArgs, makeMoveArgs, + prettyPrintToken, + testDeactivate, user1MintArtifactPlanet, - ZERO_ADDRESS, } from './utils/TestUtils'; import { defaultWorldFixture, World } from './utils/TestWorld'; import { ARTIFACT_PLANET_1, LVL0_PLANET, LVL0_PLANET_DEAD_SPACE, + LVL1_ASTEROID_1, LVL3_SPACETIME_1, LVL3_SPACETIME_2, LVL3_SPACETIME_3, LVL3_UNOWNED_NEBULA, LVL4_UNOWNED_DEEP_SPACE, LVL6_SPACETIME, - SPACE_PERLIN, SPAWN_PLANET_1, SPAWN_PLANET_2, + ZERO_PLANET, } from './utils/WorldConstants'; describe('DarkForestArtifacts', function () { @@ -54,15 +54,17 @@ describe('DarkForestArtifacts', function () { await increaseBlockchainTime(); // Move the Gear ship into position - const gearShip = (await world.user1Core.getArtifactsOnPlanet(SPAWN_PLANET_1.id)).find( - (a) => a.artifact.artifactType === ArtifactType.ShipGear + const gearShip = (await world.user1Core.getSpaceshipsOnPlanet(SPAWN_PLANET_1.id)).find( + (ship) => ship.spaceshipType === SpaceshipType.ShipGear ); - const gearId = gearShip?.artifact.id; + + const gearId = gearShip?.id; await world.user1Core.move( ...makeMoveArgs(SPAWN_PLANET_1, ARTIFACT_PLANET_1, 100, 0, 0, gearId) ); await increaseBlockchainTime(); - await world.user1Core.refreshPlanet(ARTIFACT_PLANET_1.id); + const tx = await world.user1Core.refreshPlanet(ARTIFACT_PLANET_1.id); + await tx.wait(); // Conquer another planet for artifact storage await conquerUnownedPlanet(world, world.user1Core, SPAWN_PLANET_1, LVL0_PLANET_DEAD_SPACE); @@ -75,171 +77,207 @@ describe('DarkForestArtifacts', function () { world = await loadFixture(worldFixture); }); - async function getArtifactsOnPlanet(world: World, locationId: BigNumberish) { - return (await world.contract.getArtifactsOnPlanet(locationId)) - .map((metadata) => metadata.artifact) - .filter((artifact) => artifact.artifactType < ArtifactType.ShipMothership); - } - - it('be able to mint artifact on ruins, activate/buff, deactivate/debuff', async function () { - const statSumInitial = getStatSum(await world.contract.planets(ARTIFACT_PLANET_1.id)); - - await user1MintArtifactPlanet(world.user1Core); - - const statSumAfterFound = getStatSum(await world.contract.planets(ARTIFACT_PLANET_1.id)); - - // artifact should be on planet - const artifactsOnPlanet = await getArtifactsOnPlanet(world, ARTIFACT_PLANET_1.id); - expect(artifactsOnPlanet.length).to.be.equal(1); - - // artifact should be owned by contract - expect(artifactsOnPlanet[0].discoverer).to.eq(world.user1.address); + describe('it tests basic artifact actions', function () { + it('encodes and decodes artifact', async function () { + // Must be valid options + const tokenType = TokenType.Artifact; + const rarity = ArtifactRarity.Legendary; + const artifactType = ArtifactType.Colossus; + const planetBiome = Biome.DESERT; + const res = await world.contract.createArtifactId(rarity, artifactType, planetBiome); + const a = await world.contract.getArtifactFromId(res); + expect(tokenType).to.equal(Number(a.tokenType)); + expect(rarity).to.equal(Number(a.rarity)); + expect(artifactType).to.equal(Number(a.artifactType)); + expect(planetBiome).to.equal(Number(a.planetBiome)); + }); + it('encodes and decodes spaceship', async function () { + // Must be valid options + const tokenType = TokenType.Spaceship; + const spaceshipType = 2; + const res = await world.contract.createSpaceshipId(spaceshipType); + const a = await world.contract.getSpaceshipFromId(res); + expect(tokenType).to.equal(Number(a.tokenType)); + expect(spaceshipType).to.equal(Number(a.spaceshipType)); + }); + // This test will fail if the artifact is special. + it('be able to mint artifact on ruins, activate/buff, deactivate/debuff', async function () { + const statSumInitial = getStatSum(await world.contract.planets(ARTIFACT_PLANET_1.id)); - // let's update the planet to be one of the basic artifacts, so that - // we know it's definitely going to buff the planet in some way. also, - // this prevents the artifact from being one that requires valid parameter - // in order to activate - const updatedArtifact = Object.assign({}, artifactsOnPlanet[0]); - updatedArtifact.artifactType = 0; - await world.contract.updateArtifact(updatedArtifact); + const artifactId = await createArtifact( + world.contract, + world.user1.address, + ZERO_PLANET, + ArtifactType.Colossus + ); - // planet should be buffed after discovered artifact - await world.user1Core.activateArtifact(ARTIFACT_PLANET_1.id, artifactsOnPlanet[0].id, 0); - const statSumAfterActivation = getStatSum(await world.contract.planets(ARTIFACT_PLANET_1.id)); + const statSumAfterFound = getStatSum(await world.contract.planets(ARTIFACT_PLANET_1.id)); + await world.user1Core.activateArtifact(ARTIFACT_PLANET_1.id, artifactId, 0); - // planet buff should be removed after artifact deactivated - await world.user1Core.deactivateArtifact(ARTIFACT_PLANET_1.id); - const statSumAfterDeactivate = getStatSum(await world.contract.planets(ARTIFACT_PLANET_1.id)); + prettyPrintToken(await world.user1Core.getArtifactFromId(artifactId)); - expect(statSumAfterActivation).to.not.be.within(statSumInitial - 5, statSumInitial + 5); - expect(statSumAfterActivation).to.not.be.within( - statSumAfterDeactivate - 5, - statSumAfterDeactivate + 5 - ); - expect(statSumAfterDeactivate).to.be.within(statSumInitial - 5, statSumInitial + 5); - expect(statSumAfterFound).to.be.within(statSumInitial - 5, statSumInitial + 5); - }); + // artifact and gear should be on planet. Gear is 0 and Artifact is 1. + const artifactsOnPlanet = await getArtifactsOnPlanet(world, ARTIFACT_PLANET_1.id); + expect(artifactsOnPlanet.length).to.be.equal(1); - it('cannot prospect multiple times, cannot find artifact more than 256 blocks after prospecting', async function () { - await world.user1Core.prospectPlanet(ARTIFACT_PLANET_1.id); + // artifact should be owned by contract + artifactsOnPlanet.map(async (a) => { + expect(await world.contract.balanceOf(world.contract.address, a.id)).to.equal(1); + }); - await expect(world.user1Core.prospectPlanet(ARTIFACT_PLANET_1.id)).to.be.revertedWith( - 'this planet has already been prospected' - ); + // planet should be buffed after discovered artifact + const activeArtifact = await world.user1Core.getActiveArtifactOnPlanet(ARTIFACT_PLANET_1.id); + prettyPrintToken(activeArtifact); - for (let i = 0; i < 256; i++) { - await increaseBlockchainTime(); - } + const statSumAfterActivation = getStatSum(await world.contract.planets(ARTIFACT_PLANET_1.id)); - await expect( - world.user1Core.findArtifact(...makeFindArtifactArgs(ARTIFACT_PLANET_1)) - ).to.be.revertedWith('planet prospect expired'); - }); + // planet buff should be removed after artifact deactivated + await world.user1Core.deactivateArtifact(ARTIFACT_PLANET_1.id); + const statSumAfterDeactivate = getStatSum(await world.contract.planets(ARTIFACT_PLANET_1.id)); - it('should return a correct token uri for a minted artifact', async function () { - await world.user1Core.prospectPlanet(ARTIFACT_PLANET_1.id); - await increaseBlockchainTime(); - await world.user1Core.findArtifact(...makeFindArtifactArgs(ARTIFACT_PLANET_1)); + expect(statSumAfterActivation).to.not.be.within(statSumInitial - 5, statSumInitial + 5); + expect(statSumAfterActivation).to.not.be.within( + statSumAfterDeactivate - 5, + statSumAfterDeactivate + 5 + ); + expect(statSumAfterDeactivate).to.be.within(statSumInitial - 5, statSumInitial + 5); + expect(statSumAfterFound).to.be.within(statSumInitial - 5, statSumInitial + 5); + }); - const artifactsOnPlanet = await world.contract.planetArtifacts(ARTIFACT_PLANET_1.id); - const tokenUri = await world.contract.tokenURI(artifactsOnPlanet[0]); + it('cannot prospect multiple times, cannot find artifact more than 256 blocks after prospecting', async function () { + await world.user1Core.prospectPlanet(ARTIFACT_PLANET_1.id); - const networkId = hre.network.config.chainId; - const contractAddress = world.contract.address; + await expect(world.user1Core.prospectPlanet(ARTIFACT_PLANET_1.id)).to.be.revertedWith( + 'this planet has already been prospected' + ); - expect(tokenUri).to.eq( - `https://nft-test.zkga.me/token-uri/artifact/${networkId}-${contractAddress}/` + - artifactsOnPlanet[0] - ); - }); + await mine(256); - it("should not be able to deposit an artifact you don't own", async function () { - const newArtifactId = await user1MintArtifactPlanet(world.user1Core); + await expect( + world.user1Core.findArtifact(...makeFindArtifactArgs(ARTIFACT_PLANET_1)) + ).to.be.revertedWith('planet prospect expired'); + }); - // user1 moves artifact and withdraws - await world.user1Core.move( - ...makeMoveArgs(ARTIFACT_PLANET_1, LVL3_SPACETIME_1, 0, 50000, 0, newArtifactId) - ); + it('should return a correct token uri for a minted artifact', async function () { + const artifactId = await user1MintArtifactPlanet(world.user1Core); + prettyPrintToken(await world.contract.getArtifactFromId(artifactId)); + const tokenUri = await world.contract.uri(artifactId); - world.user1Core.withdrawArtifact(LVL3_SPACETIME_1.id, newArtifactId); + const networkId = hre.network.config.chainId; + const contractAddress = world.contract.address; - // user2 should not be able to deposit artifact - await expect( - world.user2Core.depositArtifact(LVL3_SPACETIME_2.id, newArtifactId) - ).to.be.revertedWith('you can only deposit artifacts you own'); - }); + expect(tokenUri).to.eq( + `https://nft-test.zkga.me/token-uri/artifact/${networkId}-${contractAddress}/` + artifactId + ); + }); - it('should be able to move an artifact from a planet you own', async function () { - const newArtifactId = await user1MintArtifactPlanet(world.user1Core); + it("should not be able to activate an artifact you don't own", async function () { + const newArtifactId = await createArtifact( + world.contract, + world.user1.address, + ZERO_PLANET, + ArtifactType.Colossus + ); + // user2 should not be able to deposit artifact + await expect( + world.user2Core.activateArtifact(LVL3_SPACETIME_2.id, newArtifactId, 0) + ).to.be.revertedWith('you can only activate artifacts you own or on planet'); + }); - let artifactsOnRuins = await getArtifactsOnPlanet(world, ARTIFACT_PLANET_1.id); - let artifactsOnSpawn = await getArtifactsOnPlanet(world, SPAWN_PLANET_1.id); + it('should be able to move an artifact from a planet you own', async function () { + console.log('init', (await world.contract.planets(ARTIFACT_PLANET_1.id)).isInitialized); + const newArtifactId = await createArtifact( + world.contract, + world.user1.address, + ZERO_PLANET, + ArtifactType.Monolith + ); - // ruins should have artifact, spawn planet should not. - expect(artifactsOnRuins.length).to.eq(1); - expect(artifactsOnSpawn.length).to.eq(0); + expect(await world.contract.tokenExists(world.user1.address, newArtifactId)).to.equal(true); - // after finding artifact, planet's popCap might get buffed - // so let it fill up again - await increaseBlockchainTime(); + prettyPrintToken(await world.contract.getArtifactFromId(newArtifactId)); - // move artifact; check that artifact is placed on voyage - const moveTx = await world.user1Core.move( - ...makeMoveArgs(ARTIFACT_PLANET_1, SPAWN_PLANET_1, 10, 50000, 0, newArtifactId) - ); - const moveReceipt = await moveTx.wait(); - const voyageId = moveReceipt.events?.[0].args?.[1]; // emitted by ArrivalQueued - const artifactPreArrival = await world.contract.getArtifactById(newArtifactId); - expect(artifactPreArrival.voyageId).to.eq(voyageId); - expect(artifactPreArrival.locationId).to.eq(0); - - // when moving, both the ruins and the spawn planet should not have artifacts - artifactsOnRuins = await getArtifactsOnPlanet(world, ARTIFACT_PLANET_1.id); - artifactsOnSpawn = await getArtifactsOnPlanet(world, SPAWN_PLANET_1.id); - expect(artifactsOnRuins.length).to.eq(0); - expect(artifactsOnSpawn.length).to.eq(0); - - // fast forward to arrival - await increaseBlockchainTime(); - await world.user1Core.refreshPlanet(SPAWN_PLANET_1.id); - - // check artifact is on the new planet - const artifactPostArrival = await world.contract.getArtifactById(newArtifactId); - expect(artifactPostArrival.voyageId).to.eq(0); - expect(artifactPostArrival.locationId).to.eq(SPAWN_PLANET_1.id); - artifactsOnRuins = await getArtifactsOnPlanet(world, ARTIFACT_PLANET_1.id); - artifactsOnSpawn = await getArtifactsOnPlanet(world, SPAWN_PLANET_1.id); - expect(artifactsOnRuins.length).to.eq(0); - expect(artifactsOnSpawn.length).to.eq(1); - }); + await world.user1Core.activateArtifact(ARTIFACT_PLANET_1.id, newArtifactId, 0); + expect(await world.contract.tokenExists(world.contract.address, newArtifactId)).to.equal( + true + ); - it('should not be able to move more than some max amount of artifacts to a planet', async function () { - await conquerUnownedPlanet(world, world.user1Core, SPAWN_PLANET_1, LVL3_SPACETIME_1); + await world.user1Core.deactivateArtifact(ARTIFACT_PLANET_1.id); + // after finding artifact, planet's popCap might get buffed + // so let it fill up again + await increaseBlockchainTime(); - const maxArtifactsOnPlanet = 4; - for (let i = 0; i <= maxArtifactsOnPlanet; i++) { - // place an artifact on the trading post - const newTokenId = hexToBigNumber(i + 1 + ''); - await world.contract.createArtifact({ - tokenId: newTokenId, - discoverer: world.user1.address, - planetId: 1, - rarity: 1, - biome: 1, - artifactType: 5, - owner: world.user1.address, - controller: ZERO_ADDRESS, - }); - await world.user1Core.depositArtifact(LVL3_SPACETIME_1.id, newTokenId); + // move artifact; check that artifact is placed on voyage + const moveTx = await world.user1Core.move( + ...makeMoveArgs(ARTIFACT_PLANET_1, SPAWN_PLANET_1, 10, 50000, 0, newArtifactId) + ); + const moveReceipt = await moveTx.wait(); + const voyageId = moveReceipt.events?.[0].args?.[1]; // emitted by ArrivalQueued + await increaseBlockchainTime(); - // wait for the planet to fill up and download its stats + await world.contract.refreshPlanet(ARTIFACT_PLANET_1.id); + const oldLocArtifacts = await getArtifactsOnPlanet(world, ARTIFACT_PLANET_1.id); + expect(oldLocArtifacts.length).to.equal(0); + // confirming that artifact is on a voyage by checking that its no longer at the old + // destination, and that its id is on the current voyage. + const arrivalData = await world.contract.getPlanetArrival(voyageId); + expect(arrivalData.carriedArtifactId).to.equal(newArtifactId); + + // when moving, both the ruins and the spawn planet should not have artifacts + let artifactsOnRuins = await getArtifactsOnPlanet(world, ARTIFACT_PLANET_1.id); + let artifactsOnSpawn = await getArtifactsOnPlanet(world, SPAWN_PLANET_1.id); + expect(artifactsOnRuins.length).to.eq(0); + expect(artifactsOnSpawn.length).to.eq(0); + + // fast forward to arrival await increaseBlockchainTime(); - await world.user1Core.refreshPlanet(LVL3_SPACETIME_1.id); - const tradingPost2Planet = await world.user1Core.planets(LVL3_SPACETIME_1.id); + await world.user1Core.refreshPlanet(SPAWN_PLANET_1.id); + + // check artifact is on the new planet. Hard to test artifact is NOT on a voyage. but if it + // exists on a planet, it is not on a voyage. + artifactsOnRuins = await getArtifactsOnPlanet(world, ARTIFACT_PLANET_1.id); + artifactsOnSpawn = await getArtifactsOnPlanet(world, SPAWN_PLANET_1.id); + expect(artifactsOnRuins.length).to.eq(0); + expect(artifactsOnSpawn.length).to.eq(1); + }); + + it('should not be able to move more than some max amount of artifacts to a planet', async function () { + await conquerUnownedPlanet(world, world.user1Core, SPAWN_PLANET_1, LVL3_SPACETIME_1); - if (i > maxArtifactsOnPlanet) { - await expect( - world.user1Core.move( + const maxArtifactsOnPlanet = 4; + for (let i = 0; i <= maxArtifactsOnPlanet; i++) { + // place an artifact on the trading post + const newTokenId = await createArtifact( + world.contract, + world.user1.address, + ZERO_PLANET, + ArtifactType.Monolith + ); + await world.user1Core.activateArtifact(LVL3_SPACETIME_1.id, newTokenId, 0); + await world.user1Core.deactivateArtifact(LVL3_SPACETIME_1.id); + // wait for the planet to fill up and download its stats + await increaseBlockchainTime(); + await world.user1Core.refreshPlanet(LVL3_SPACETIME_1.id); + const tradingPost2Planet = await world.user1Core.planets(LVL3_SPACETIME_1.id); + + if (i > maxArtifactsOnPlanet) { + await expect( + world.user1Core.move( + ...makeMoveArgs( + LVL3_SPACETIME_1, + LVL0_PLANET_DEAD_SPACE, + 0, + tradingPost2Planet.population.toNumber() - 1, + 0, + newTokenId + ) + ) + ).to.be.revertedWith( + 'the planet you are moving an artifact to can have at most 5 artifacts on it' + ); + } else { + // move the artifact from the trading post + await world.user1Core.move( ...makeMoveArgs( LVL3_SPACETIME_1, LVL0_PLANET_DEAD_SPACE, @@ -248,330 +286,170 @@ describe('DarkForestArtifacts', function () { 0, newTokenId ) - ) - ).to.be.revertedWith( - 'the planet you are moving an artifact to can have at most 5 artifacts on it' - ); - } else { - // move the artifact from the trading post - await world.user1Core.move( - ...makeMoveArgs( - LVL3_SPACETIME_1, - LVL0_PLANET_DEAD_SPACE, - 0, - tradingPost2Planet.population.toNumber() - 1, - 0, - newTokenId - ) - ); - await increaseBlockchainTime(); - await world.user1Core.refreshPlanet(LVL0_PLANET_DEAD_SPACE.id); - const artifactsOnPlanet = await getArtifactsOnPlanet(world, LVL0_PLANET_DEAD_SPACE.id); - expect(artifactsOnPlanet.length).to.eq(i + 1); + ); + await increaseBlockchainTime(); + await world.user1Core.refreshPlanet(LVL0_PLANET_DEAD_SPACE.id); + const artifactsOnPlanet = await getArtifactsOnPlanet(world, LVL0_PLANET_DEAD_SPACE.id); + expect(artifactsOnPlanet.length).to.eq(i + 1); + } } - } - }); - - it("should be able to conquer another player's planet and move their artifact", async function () { - const newArtifactId = await user1MintArtifactPlanet(world.user1Core); - - // after finding artifact, planet's popCap might get buffed - // so let it fill up again - await increaseBlockchainTime(); - - const artifactPlanetPopCap = ( - await world.contract.planets(ARTIFACT_PLANET_1.id) - ).populationCap.toNumber(); - - await world.user1Core.move( - ...makeMoveArgs( - ARTIFACT_PLANET_1, - SPAWN_PLANET_1, - 10, - Math.floor(artifactPlanetPopCap * 0.999), // if only 0.99 it's still untakeable, bc high def - 0 - ) - ); - - // steal planet - await world.user2Core.move(...makeMoveArgs(SPAWN_PLANET_2, ARTIFACT_PLANET_1, 0, 50000, 0)); - await increaseBlockchainTime(); - - // move artifact - await world.user2Core.move( - ...makeMoveArgs(ARTIFACT_PLANET_1, LVL3_SPACETIME_2, 0, 50000, 0, newArtifactId) - ); - await increaseBlockchainTime(); - - // verify that artifact was moved - await world.user2Core.withdrawArtifact(LVL3_SPACETIME_2.id, newArtifactId); - const artifacts = await getArtifactsOwnedBy(world.contract, world.user2.address); - - expect(artifacts.length).to.be.equal(1); - }); - - it('not be able to prospect for an artifact on planets that are not ruins', async function () { - await expect(world.user1Core.prospectPlanet(SPAWN_PLANET_1.id)).to.be.revertedWith( - "you can't find an artifact on this planet" - ); - }); - - it('should mint randomly', async function () { - // This can take upwards of 90000ms in CI - this.timeout(0); - - await conquerUnownedPlanet(world, world.user1Core, SPAWN_PLANET_1, LVL3_SPACETIME_1); - - /* eslint-disable @typescript-eslint/no-explicit-any */ - let artifacts: any; - let prevLocation = SPAWN_PLANET_1; - - for (let i = 0; i < 20; i++) { - // byte #8 is 18_16 = 24_10 so it's a ruins planet - const randomHex = - `00007c2512896efb182d462faee0000fb33d58930eb9e6b4fbae6d048e9c44` + - (i >= 10 ? i.toString()[0] : 0) + - '' + - (i % 10); - - const planetWithArtifactLoc = new TestLocation({ - hex: randomHex, - perlin: SPACE_PERLIN, - distFromOrigin: 1998, - }); - - await world.contract.adminInitializePlanet( - planetWithArtifactLoc.id, - planetWithArtifactLoc.perlin - ); + }); - await world.contract.adminGiveSpaceShip( - planetWithArtifactLoc.id, + it("should be able to conquer another player's planet and move their artifact", async function () { + const artifactId = await createArtifact( + world.contract, world.user1.address, - ArtifactType.ShipGear + ARTIFACT_PLANET_1, + ArtifactType.Monolith ); - + await world.user1Core.activateArtifact(ARTIFACT_PLANET_1.id, artifactId, 0); + // after finding artifact, planet's popCap might get buffed + // so let it fill up again await increaseBlockchainTime(); - await world.user1Core.move(...makeMoveArgs(prevLocation, planetWithArtifactLoc, 0, 80000, 0)); // move 80000 from asteroids but 160000 from ruins since ruins are higher level - await increaseBlockchainTime(); - - await world.user1Core.prospectPlanet(planetWithArtifactLoc.id); - await increaseBlockchainTime(); - - await world.user1Core.findArtifact(...makeFindArtifactArgs(planetWithArtifactLoc)); - await increaseBlockchainTime(); - - const artifactsOnPlanet = await getArtifactsOnPlanet(world, planetWithArtifactLoc.id); - const artifactId = artifactsOnPlanet[0].id; + const artifactPlanetPopCap = ( + await world.contract.planets(ARTIFACT_PLANET_1.id) + ).populationCap.toNumber(); await world.user1Core.move( - ...makeMoveArgs(planetWithArtifactLoc, LVL3_SPACETIME_1, 0, 40000, 0, artifactId) + ...makeMoveArgs( + ARTIFACT_PLANET_1, + SPAWN_PLANET_1, + 10, + Math.floor(artifactPlanetPopCap * 0.999), // if only 0.99 it's still untakeable, bc high def + 0 + ) ); - await world.user1Core.withdrawArtifact(LVL3_SPACETIME_1.id, artifactId); - artifacts = await getArtifactsOwnedBy(world.contract, world.user1.address); - - expect(artifacts[artifacts.length - 1].planetBiome).to.eq(4); // tundra - expect(artifacts[artifacts.length - 1].discoverer).to.eq(world.user1.address); - expect(artifacts[artifacts.length - 1].rarity).to.be.at.least(1); - - prevLocation = planetWithArtifactLoc; - } - const artifactTypeSet = new Set(); - - for (let i = 0; i < artifacts.length; i++) { - artifactTypeSet.add(artifacts[i].artifactType); - } - - expect(artifactTypeSet.size).to.be.greaterThan(1); - }); - - it('should not mint an artifact on the same planet twice', async function () { - await world.user1Core.prospectPlanet(ARTIFACT_PLANET_1.id); - await increaseBlockchainTime(); - await world.user1Core.findArtifact(...makeFindArtifactArgs(ARTIFACT_PLANET_1)); - await increaseBlockchainTime(); - await expect( - world.user1Core.findArtifact(...makeFindArtifactArgs(ARTIFACT_PLANET_1)) - ).to.be.revertedWith('artifact already minted from this planet'); - }); - - it('should not be able to move an activated artifact', async function () { - const artifactId = await createArtifactOnPlanet( - world.contract, - world.user1.address, - ARTIFACT_PLANET_1, - ArtifactType.Monolith - ); - await world.user1Core.activateArtifact(ARTIFACT_PLANET_1.id, artifactId, 0); - - await expect( - world.user1Core.move( - ...makeMoveArgs(ARTIFACT_PLANET_1, SPAWN_PLANET_1, 10, 50000, 0, artifactId) - ) - ).to.be.revertedWith('you cannot take an activated artifact off a planet'); - }); + // steal planet + await world.user2Core.move(...makeMoveArgs(SPAWN_PLANET_2, ARTIFACT_PLANET_1, 0, 50000, 0)); + await increaseBlockchainTime(); - it("should not be able to move an artifact from a planet it's not on", async function () { - const newArtifactId = await user1MintArtifactPlanet(world.user1Core); - // after finding artifact, planet's popCap might get buffed - // so let it fill up again - await increaseBlockchainTime(); + await world.user2Core.deactivateArtifact(ARTIFACT_PLANET_1.id); - // move artifact - world.user1Core.move( - ...makeMoveArgs(ARTIFACT_PLANET_1, SPAWN_PLANET_1, 10, 50000, 0, newArtifactId) - ); + // move artifact + await world.user2Core.move( + ...makeMoveArgs(ARTIFACT_PLANET_1, LVL3_SPACETIME_2, 0, 50000, 0, artifactId) + ); + await increaseBlockchainTime(); + await world.user1Core.refreshPlanet(LVL3_SPACETIME_2.id); + // verify that artifact was moved + const artifacts = await world.user2Core.getArtifactsOnPlanet(LVL3_SPACETIME_2.id); - // try moving artifact again; should fail - await expect( - world.user1Core.move( - ...makeMoveArgs(ARTIFACT_PLANET_1, SPAWN_PLANET_1, 10, 50000, 0, newArtifactId) - ) - ).to.be.revertedWith('this artifact was not present on this planet'); + expect(artifacts.length).to.be.equal(1); + }); - // try moving nonexistent artifact - await expect( - world.user1Core.move(...makeMoveArgs(ARTIFACT_PLANET_1, SPAWN_PLANET_1, 10, 50000, 0, 12345)) - ).to.be.revertedWith('this artifact was not present on this planet'); - }); + it('not be able to prospect for an artifact on planets that are not ruins', async function () { + await expect(world.user1Core.prospectPlanet(SPAWN_PLANET_1.id)).to.be.revertedWith( + "you can't find an artifact on this planet" + ); + }); - describe('trading post', function () { - it('should be able to withdraw from / deposit onto trading posts you own', async function () { - await conquerUnownedPlanet(world, world.user1Core, SPAWN_PLANET_1, LVL3_SPACETIME_3); + it('should not mint an artifact on the same planet twice', async function () { + await world.user1Core.prospectPlanet(ARTIFACT_PLANET_1.id); await increaseBlockchainTime(); + await world.user1Core.findArtifact(...makeFindArtifactArgs(ARTIFACT_PLANET_1)); + await increaseBlockchainTime(); + await expect( + world.user1Core.findArtifact(...makeFindArtifactArgs(ARTIFACT_PLANET_1)) + ).to.be.revertedWith('artifact already minted from this planet'); + }); - const newArtifactId = await user1MintArtifactPlanet(world.user1Core); - - // move artifact to LVL3_SPACETIME_1 - await world.user1Core.move( - ...makeMoveArgs(ARTIFACT_PLANET_1, LVL3_SPACETIME_1, 0, 50000, 0, newArtifactId) + it('should not be able to move an activated artifact', async function () { + const artifactId = await createArtifact( + world.contract, + world.user1.address, + ZERO_PLANET, + ArtifactType.Monolith ); - await world.user1Core.refreshPlanet(LVL3_SPACETIME_1.id); - - // artifact should be on LVL3_SPACETIME_1 - let artifact = await world.contract.getArtifactById(newArtifactId); - let artifactsOnTP1 = await world.contract.getArtifactsOnPlanet(LVL3_SPACETIME_1.id); - let artifactsOnTP2 = await world.contract.getArtifactsOnPlanet(LVL3_SPACETIME_3.id); - await expect(artifact.locationId).to.eq(LVL3_SPACETIME_1.id); - await expect(artifactsOnTP1.length).to.eq(1); - await expect(artifactsOnTP2.length).to.eq(0); - - // withdraw from LVL3_SPACETIME_1 - await world.user1Core.withdrawArtifact(LVL3_SPACETIME_1.id, newArtifactId); - - // artifact should be on voyage - artifact = await world.contract.getArtifactById(newArtifactId); - artifactsOnTP1 = await world.contract.getArtifactsOnPlanet(LVL3_SPACETIME_1.id); - artifactsOnTP2 = await world.contract.getArtifactsOnPlanet(LVL3_SPACETIME_3.id); - await expect(artifact.locationId).to.eq(0); - await expect(artifactsOnTP1.length).to.eq(0); - await expect(artifactsOnTP2.length).to.eq(0); - - // deposit onto LVL3_SPACETIME_3 - await world.user1Core.depositArtifact(LVL3_SPACETIME_3.id, newArtifactId); - - // artifact should be on LVL3_SPACETIME_3 - artifact = await world.contract.getArtifactById(newArtifactId); - artifactsOnTP1 = await world.contract.getArtifactsOnPlanet(LVL3_SPACETIME_1.id); - artifactsOnTP2 = await world.contract.getArtifactsOnPlanet(LVL3_SPACETIME_3.id); - await expect(artifact.locationId).to.eq(LVL3_SPACETIME_3.id); - await expect(artifactsOnTP1.length).to.eq(0); - await expect(artifactsOnTP2.length).to.eq(1); + await world.user1Core.activateArtifact(ARTIFACT_PLANET_1.id, artifactId, 0); + + await expect( + world.user1Core.move( + ...makeMoveArgs(ARTIFACT_PLANET_1, SPAWN_PLANET_1, 10, 50000, 0, artifactId) + ) + ).to.be.revertedWith('you cannot take an activated artifact off a planet'); }); - it("should not be able to withdraw from / deposit onto trading post you don't own", async function () { - const newArtifactId = await user1MintArtifactPlanet(world.user1Core); + it("should not be able to move an artifact from a planet it's not on", async function () { + const newArtifactId = await createArtifact( + world.contract, + world.user1.address, + ZERO_PLANET, + ArtifactType.Monolith + ); + await world.user1Core.activateArtifact(ARTIFACT_PLANET_1.id, newArtifactId, 0); + await world.user1Core.deactivateArtifact(ARTIFACT_PLANET_1.id); + // after finding artifact, planet's popCap might get buffed + // so let it fill up again + await increaseBlockchainTime(); // move artifact await world.user1Core.move( - ...makeMoveArgs(ARTIFACT_PLANET_1, LVL3_SPACETIME_1, 0, 50000, 0, newArtifactId) + ...makeMoveArgs(ARTIFACT_PLANET_1, SPAWN_PLANET_1, 10, 50000, 0, newArtifactId) ); - // user2 should not be able to withdraw from LVL3_SPACETIME_1 + // try moving artifact again; should fail await expect( - world.user2Core.withdrawArtifact(LVL3_SPACETIME_1.id, newArtifactId) - ).to.be.revertedWith('you can only withdraw from a planet you own'); + world.user1Core.move( + ...makeMoveArgs(ARTIFACT_PLANET_1, SPAWN_PLANET_1, 10, 50000, 0, newArtifactId) + ) + ).to.be.revertedWith('this artifact was not present on this planet'); - // user1 should not be able to deposit onto LVL3_SPACETIME_2 - world.user1Core.withdrawArtifact(LVL3_SPACETIME_1.id, newArtifactId); + // try moving nonexistent artifact await expect( - world.user1Core.depositArtifact(LVL3_SPACETIME_2.id, newArtifactId) - ).to.be.revertedWith('you can only deposit on a planet you own'); + world.user1Core.move( + ...makeMoveArgs(ARTIFACT_PLANET_1, SPAWN_PLANET_1, 10, 50000, 0, 12345) + ) + ).to.be.revertedWith('cannot move token of this type'); }); + it('should be able to activate, deactivate, move, activate', async function () { + await conquerUnownedPlanet(world, world.user1Core, SPAWN_PLANET_1, LVL1_ASTEROID_1); + const artifactId = await createArtifact( + world.contract, + world.user1.address, + ZERO_PLANET, + ArtifactType.Monolith + ); + await world.user1Core.activateArtifact(ARTIFACT_PLANET_1.id, artifactId, 0); - it('should not be able to withdraw an artifact from a trading post that is not on the trading post', async function () { - const newArtifactId = await user1MintArtifactPlanet(world.user1Core); + await world.user1Core.deactivateArtifact(ARTIFACT_PLANET_1.id); - // should not be able to withdraw newArtifactId from LVL3_SPACETIME_1 - await expect( - world.user1Core.withdrawArtifact(LVL3_SPACETIME_1.id, newArtifactId) - ).to.be.revertedWith('this artifact is not on this planet'); - }); + await world.user1Core.move( + ...makeMoveArgs(ARTIFACT_PLANET_1, LVL1_ASTEROID_1, 10, 50000, 0, artifactId) + ); - it('should not be able to withdraw/deposit onto a planet that is not a trading post', async function () { - await conquerUnownedPlanet(world, world.user1Core, SPAWN_PLANET_1, LVL0_PLANET); await increaseBlockchainTime(); - const newArtifactId = await user1MintArtifactPlanet(world.user1Core); - - // should not be able to withdraw from ruins (which are not trading posts) - await expect( - world.user2Core.withdrawArtifact(ARTIFACT_PLANET_1.id, newArtifactId) - ).to.be.revertedWith('can only withdraw from trading posts'); - - // move artifact and withdraw - await world.user1Core.move( - ...makeMoveArgs(ARTIFACT_PLANET_1, LVL3_SPACETIME_1, 0, 50000, 0, newArtifactId) + await world.user1Core.activateArtifact(LVL1_ASTEROID_1.id, artifactId, 0); + expect((await world.user1Core.getActiveArtifactOnPlanet(LVL1_ASTEROID_1.id)).id).to.equal( + artifactId ); - world.user1Core.withdrawArtifact(LVL3_SPACETIME_1.id, newArtifactId); - - // should not be able to deposit onto LVL0_PLANET (which is regular planet and not trading post) - await expect( - world.user1Core.depositArtifact(LVL0_PLANET.id, newArtifactId) - ).to.be.revertedWith('can only deposit on trading posts'); }); + }); - it('should not be able to withdraw/deposit a high level artifact onto low level trading post', async function () { + describe('trading post', function () { + it('should not be able to activate a high level artifact onto low level trading post', async function () { await conquerUnownedPlanet(world, world.user1Core, LVL3_SPACETIME_1, LVL6_SPACETIME); await increaseBlockchainTime(); // allow planets to fill up energy again - const newTokenId = hexToBigNumber('1'); - await world.contract.createArtifact({ - tokenId: newTokenId, - discoverer: world.user1.address, - planetId: 1, // planet id - rarity: 4, // rarity - biome: 1, // biome - artifactType: 1, - owner: world.user1.address, - controller: ZERO_ADDRESS, - }); - // deposit fails on low level trading post, succeeds on high level trading post - await expect( - world.user1Core.depositArtifact(LVL3_SPACETIME_1.id, newTokenId) - ).to.be.revertedWith('spacetime rip not high enough level to deposit this artifact'); - world.user1Core.depositArtifact(LVL6_SPACETIME.id, newTokenId); - - // withdraw fails on low level trading post - await world.user1Core.move( - ...makeMoveArgs(LVL6_SPACETIME, LVL3_SPACETIME_1, 0, 250000000, 0, newTokenId) + const newTokenId = await createArtifact( + world.contract, + world.user1.address, + ARTIFACT_PLANET_1, + ArtifactType.BlackDomain, + ArtifactRarity.Common ); - await expect( - world.user1Core.withdrawArtifact(LVL3_SPACETIME_1.id, newTokenId) - ).to.be.revertedWith('spacetime rip not high enough level to withdraw this artifact'); - // withdraw succeeds on high level post - await world.user1Core.move( - ...makeMoveArgs(LVL3_SPACETIME_1, LVL6_SPACETIME, 0, 500000, 0, newTokenId) - ); - await world.user1Core.withdrawArtifact(LVL6_SPACETIME.id, newTokenId); + // activate fails on low level trading post, succeeds on high level trading post + await expect( + world.user1Core.activateArtifact(LVL3_SPACETIME_1.id, newTokenId, 0) + ).to.be.revertedWith('artifact is not powerful enough to apply effect to this planet level'); }); }); describe('wormhole', function () { - it('should increase movement speed, in both directions', async function () { + it('should increase movement speed, in both directions, and decrease after deactivation', async function () { // This can take an upwards of 32000ms this.timeout(0); @@ -589,15 +467,20 @@ describe('DarkForestArtifacts', function () { const wormholeSpeedups = [2, 4, 8, 16, 32]; for (let i = 0; i < artifactRarities.length; i++) { - const artifactId = await createArtifactOnPlanet( + const artifactId = await createArtifact( world.contract, world.user1.address, - from, + ZERO_PLANET, ArtifactType.Wormhole, - { rarity: artifactRarities[i] as ArtifactRarity, biome: Biome.OCEAN } + artifactRarities[i] as ArtifactRarity, + Biome.OCEAN ); + prettyPrintToken(await world.contract.getArtifactFromId(artifactId)); + await world.user1Core.activateArtifact(from.id, artifactId, to.id); + // activateAndConfirm(world.user1Core, from.id, artifactId, to.id); + // move from planet with artifact to its wormhole destination await increaseBlockchainTime(); await world.user1Core.move(...makeMoveArgs(from, to, dist, shipsSent, silverSent)); @@ -608,7 +491,7 @@ describe('DarkForestArtifacts', function () { Math.floor((dist * 100) / wormholeSpeedups[i]) / fromPlanet.speed.toNumber() ); - expect(arrival.arrivalTime.sub(arrival.departureTime)).to.be.equal(expectedTime); + expect(arrival.arrivalTime.sub(arrival.departureTime).toNumber()).to.be.equal(expectedTime); // move from the wormhole destination planet back to the planet whose wormhole is pointing at // it @@ -626,6 +509,22 @@ describe('DarkForestArtifacts', function () { ); await world.user1Core.deactivateArtifact(from.id); + + // Move from planet with artifact to destination and expect speed is not boosted. + // Also move the artifact bc too many tokens; + await world.user1Core.move( + ...makeMoveArgs(from, to, dist, shipsSent, silverSent, artifactId) + ); + const fromPlanetAfterDActivate = await world.contract.planets(from.id); + const planetArrivalsAfterDActivate = await world.contract.getPlanetArrivals(to.id); + const arrivalAfterDActivate = planetArrivalsAfterDActivate[0]; + const expectedTimeAfterDActivate = Math.floor( + Math.floor(dist * 100) / fromPlanetAfterDActivate.speed.toNumber() + ); + + expect( + arrivalAfterDActivate.arrivalTime.sub(arrivalAfterDActivate.departureTime).toNumber() + ).to.be.equal(expectedTimeAfterDActivate); } }); @@ -643,26 +542,29 @@ describe('DarkForestArtifacts', function () { expect(toPlanet.owner).to.eq(world.user1.address); // create a wormhole - const newTokenId = hexToBigNumber('5'); - await world.contract.createArtifact({ - tokenId: newTokenId, - discoverer: world.user1.address, - planetId: 1, // planet id - rarity: 1, - biome: 1, // biome - artifactType: 5, // wormhole - owner: world.user1.address, - controller: ZERO_ADDRESS, - }); - const userArtifacts = await world.contract.getPlayerArtifactIds(world.user1.address); - expect(userArtifacts[0]).to.eq(newTokenId); + const artifactId = await createArtifact( + world.contract, + world.user1.address, + ZERO_PLANET, + ArtifactType.Wormhole, + ArtifactRarity.Common, + Biome.OCEAN + ); + + // Move gear bc too many artifacts on SPAWN_PLANET_1, so can't receive wormhole. + const crescentShip = (await world.user1Core.getSpaceshipsOnPlanet(SPAWN_PLANET_1.id)).find( + (ship) => ship.spaceshipType === SpaceshipType.ShipCrescent + ); - // activate the wormhole to the 2nd planet - await world.user1Core.depositArtifact(LVL3_SPACETIME_1.id, newTokenId); await world.user1Core.move( - ...makeMoveArgs(LVL3_SPACETIME_1, SPAWN_PLANET_1, 0, 500000, 0, newTokenId) + ...makeMoveArgs(SPAWN_PLANET_1, LVL0_PLANET, 0, 0, 0, crescentShip?.id) ); - await world.user1Core.activateArtifact(from.id, newTokenId, to.id); + + const userWormholeBalance = await world.contract.balanceOf(world.user1.address, artifactId); + expect(userWormholeBalance).to.eq(1); + + // activate the wormhole to the 2nd planet + await world.user1Core.activateArtifact(from.id, artifactId, to.id); const dist = 50; const shipsSent = 10000; @@ -725,37 +627,30 @@ describe('DarkForestArtifacts', function () { ); expect(planetBeforeBloomFilter.silver).to.eq(0); - const newTokenId = hexToBigNumber('1'); - await world.contract.createArtifact({ - tokenId: newTokenId, - discoverer: world.user1.address, - planetId: 1, // planet id - rarity: 1, // rarity - biome: 1, // biome - artifactType: 8, // bloom filter - owner: world.user1.address, - controller: ZERO_ADDRESS, - }); - await increaseBlockchainTime(); // so that trading post can fill up to max energy - await world.user1Core.depositArtifact(LVL3_SPACETIME_1.id, newTokenId); - await world.user1Core.move( - ...makeMoveArgs(LVL3_SPACETIME_1, SPAWN_PLANET_1, 0, 500000, 0, newTokenId) + const newTokenId = await createArtifact( + world.contract, + world.user1.address, + ZERO_PLANET, + ArtifactType.BloomFilter, + ArtifactRarity.Common, + Biome.OCEAN ); - await world.user1Core.activateArtifact(from.id, newTokenId, 0); + prettyPrintToken(await world.contract.getArtifactFromId(newTokenId)); + + await increaseBlockchainTime(); // so that trading post can fill up to max energy + await world.user1Core.activateArtifact(SPAWN_PLANET_1.id, newTokenId, 0); const planetAfterBloomFilter = await world.user1Core.planets(from.id); expect(planetAfterBloomFilter.population).to.eq(planetAfterBloomFilter.populationCap); expect(planetAfterBloomFilter.silver).to.eq(planetAfterBloomFilter.silverCap); - const bloomFilterPostActivation = await world.contract.getArtifactById(newTokenId); - + const artifactsOnRipAfterBurn = await getArtifactsOnPlanet(world, SPAWN_PLANET_1.id); + console.log(`artifacts on rip after burn`, artifactsOnRipAfterBurn); // bloom filter is immediately deactivated after activation - expect(bloomFilterPostActivation.artifact.lastActivated).to.eq( - bloomFilterPostActivation.artifact.lastDeactivated - ); + expect(artifactsOnRipAfterBurn.length).to.equal(0); // bloom filter is no longer on a planet (is instead owned by contract), and so is effectively burned - expect(bloomFilterPostActivation.locationId.toString()).to.eq('0'); + // expect(bloomFilterPostActivation.locationId.toString()).to.eq('0'); }); it("can't be used on a planet of too high level", async function () { @@ -775,22 +670,17 @@ describe('DarkForestArtifacts', function () { ); expect(planetBeforeBloomFilter.silver).to.eq(0); - const newTokenId = hexToBigNumber('1'); - await world.contract.createArtifact({ - tokenId: newTokenId, - discoverer: world.user1.address, - planetId: 1, // planet id - rarity: 1, // rarity - biome: 1, // biome - artifactType: 9, // bloom filter - owner: world.user1.address, - controller: ZERO_ADDRESS, - }); - await increaseBlockchainTime(); // so that trading post can fill up to max energy - await world.user1Core.depositArtifact(LVL3_SPACETIME_1.id, newTokenId); - await world.user1Core.move( - ...makeMoveArgs(LVL3_SPACETIME_1, LVL4_UNOWNED_DEEP_SPACE, 0, 500000, 0, newTokenId) + const newTokenId = await createArtifact( + world.contract, + world.user1.address, + ZERO_PLANET, + ArtifactType.BloomFilter, + ArtifactRarity.Common, + Biome.OCEAN ); + + prettyPrintToken(await world.contract.getArtifactFromId(newTokenId)); + await increaseBlockchainTime(); // so that trading post can fill up to max energy await expect( world.user1Core.activateArtifact(LVL4_UNOWNED_DEEP_SPACE.id, newTokenId, 0) ).to.be.revertedWith('artifact is not powerful enough to apply effect to this planet level'); @@ -811,24 +701,19 @@ describe('DarkForestArtifacts', function () { const conqueredSecondPlanet = await world.user1Core.planets(to.id); expect(conqueredSecondPlanet.owner).to.eq(world.user1.address); - const newTokenId = hexToBigNumber('1'); - await world.contract.createArtifact({ - tokenId: newTokenId, - discoverer: world.user1.address, - planetId: 1, // planet id - rarity: 1, // rarity - biome: 1, // biome - artifactType: 9, // black domain - owner: world.user1.address, - controller: ZERO_ADDRESS, - }); - await world.user1Core.depositArtifact(LVL3_SPACETIME_1.id, newTokenId); - await world.user1Core.move(...makeMoveArgs(LVL3_SPACETIME_1, to, 0, 500000, 0, newTokenId)); + const newTokenId = await createArtifact( + world.contract, + world.user1.address, + ZERO_PLANET, + ArtifactType.BlackDomain, + ArtifactRarity.Common, + Biome.OCEAN + ); await world.user1Core.activateArtifact(to.id, newTokenId, 0); - // black domain is no longer on a planet (is instead owned by contract), and so is effectively burned - const blackDomainPostActivation = await world.contract.getArtifactById(newTokenId); - expect(blackDomainPostActivation.locationId.toString()).to.eq('0'); + // black domain is no longer on a planet (is instead owned by contract), and so is effectively + // burned + testDeactivate(world, to.id); // check the planet is destroyed const newPlanet = await world.user1Core.planets(to.id); @@ -863,27 +748,108 @@ describe('DarkForestArtifacts', function () { ); expect(planetBeforeBlackDomain.silver).to.eq(0); - const newTokenId = hexToBigNumber('1'); - await world.contract.createArtifact({ - tokenId: newTokenId, - discoverer: world.user1.address, - planetId: 1, // planet id - rarity: 1, // rarity - biome: 1, // biome - artifactType: 8, // bloom filter - owner: world.user1.address, - controller: ZERO_ADDRESS, - }); - await increaseBlockchainTime(); // so that trading post can fill up to max energy - await world.user1Core.depositArtifact(LVL3_SPACETIME_1.id, newTokenId); - await world.user1Core.move( - ...makeMoveArgs(LVL3_SPACETIME_1, LVL4_UNOWNED_DEEP_SPACE, 0, 500000, 0, newTokenId) + const newTokenId = await createArtifact( + world.contract, + world.user1.address, + ZERO_PLANET, + ArtifactType.BlackDomain, + ArtifactRarity.Common, + Biome.OCEAN ); + await expect( world.user1Core.activateArtifact(LVL4_UNOWNED_DEEP_SPACE.id, newTokenId, 0) ).to.be.revertedWith('artifact is not powerful enough to apply effect to this planet level'); }); }); - // TODO: tests for photoid cannon and planetary shield? + describe('planetary shield', function () { + it('activates planetary shield, + defense, - range, then burns shield', async function () { + await conquerUnownedPlanet(world, world.user1Core, SPAWN_PLANET_1, LVL3_SPACETIME_1); + + const newTokenId = await createArtifact( + world.contract, + world.user1.address, + LVL3_SPACETIME_1, + ArtifactType.PlanetaryShield, + ArtifactRarity.Rare as ArtifactRarity, + Biome.OCEAN + ); + prettyPrintToken(await world.contract.getArtifactFromId(newTokenId)); + + const planetBeforeActivation = await world.user1Core.planets(LVL3_SPACETIME_1.id); + await activateAndConfirm(world.user1Core, LVL3_SPACETIME_1.id, newTokenId); + const planetAfterActivation = await world.user1Core.planets(LVL3_SPACETIME_1.id); + + // Boosts are applied + expect(planetBeforeActivation.defense).to.be.lessThan(planetAfterActivation.defense); + expect(planetBeforeActivation.range).to.be.greaterThan(planetAfterActivation.range); + expect(planetBeforeActivation.speed).to.be.greaterThan(planetAfterActivation.speed); + + // Burned on deactivate + await world.user1Core.deactivateArtifact(LVL3_SPACETIME_1.id); + + testDeactivate(world, LVL3_SPACETIME_3.id); + }); + }); + + describe('photoid cannon', function () { + it('activates photoid cannon, increases move speed and range, then burns photoid', async function () { + await conquerUnownedPlanet(world, world.user1Core, LVL3_SPACETIME_1, LVL6_SPACETIME); + await increaseBlockchainTime(); + + const to = LVL0_PLANET; + const dist = 50; + const forces = 40000000; // Has to be big to account for + + const rangeBoosts = [100, 200, 200, 200, 200, 200]; + // Divided by 100 to reflect effect on travel time. + const speedBoosts = [1, 5, 10, 15, 20, 25]; + const artifactRarities = [1, 2, 3, 4, 5]; + + for (let i = 0; i < artifactRarities.length; i++) { + const newTokenId = await createArtifact( + world.contract, + world.user1.address, + ZERO_PLANET, + ArtifactType.PhotoidCannon, + artifactRarities[i] as ArtifactRarity, + Biome.OCEAN + ); + prettyPrintToken(await world.contract.getArtifactFromId(newTokenId)); + + // await world.user1Core.depositArtifact(LVL6_SPACETIME.id, newTokenId); + + // Confirm photoid cannon is activated. + await activateAndConfirm(world.user1Core, LVL6_SPACETIME.id, newTokenId); + + await increaseBlockchainTime(); + + // Make a move that uses photoid cannon + await world.user1Core.move( + ...makeMoveArgs(LVL6_SPACETIME, to, dist, forces, 0, newTokenId) + ); + const fromPlanet = await world.contract.planets(LVL6_SPACETIME.id); + const planetArrivals = await world.contract.getPlanetArrivals(to.id); + const arrival = planetArrivals[0]; + + const expectedTime = Math.floor( + Math.floor((dist * 100) / speedBoosts[i + 1]) / fromPlanet.speed.toNumber() + ); + + expect(arrival.arrivalTime.sub(arrival.departureTime).toNumber()).to.be.equal(expectedTime); + + const range = (fromPlanet.range.toNumber() * rangeBoosts[i + 1]) / 100; + const popCap = fromPlanet.populationCap.toNumber(); + const decayFactor = Math.pow(2, dist / range); + const approxArriving = forces / decayFactor - 0.05 * popCap; + + expect(planetArrivals[0].popArriving.toNumber()).to.be.above(approxArriving - 1000); + expect(planetArrivals[0].popArriving.toNumber()).to.be.below(approxArriving + 1000); + + // Confirm photoid is burned + await testDeactivate(world, LVL6_SPACETIME.id); + } + }); + }); }); diff --git a/eth/test/DFLobby.test.ts b/eth/test/DFLobby.test.ts index 4648a370..1e315436 100644 --- a/eth/test/DFLobby.test.ts +++ b/eth/test/DFLobby.test.ts @@ -5,9 +5,9 @@ import hre, { ethers } from 'hardhat'; import { createArena, defaultWorldFixture, World } from './utils/TestWorld'; const _INTERFACE_ID_IERC165 = '0x01ffc9a7'; -const _INTERFACE_ID_IERC721 = '0x80ac58cd'; -const _INTERFACE_ID_IERC721METADATA = '0x5b5e139f'; -const _INTERFACE_ID_IERC721ENUMERABLE = '0x780e9d63'; +const _INTERFACE_ID_IERC1155 = '0xd9b67a26'; +const _INTERFACE_ID_IERC1155METADATA = '0x0e89341c'; +const _INTERFACE_ID_IERC1155ENUMERABLE = '0x464FCF40'; const _INTERFACE_ID_IDIAMOND_READABLE = '0x48e2b093'; const _INTERFACE_ID_IDIAMOND_WRITABLE = '0x1f931c1c'; const _INTERFACE_ID_IERC173 = '0x7f5828d0'; @@ -52,11 +52,12 @@ describe('DarkForestLobby', function () { expect(await lobby.supportsInterface(_INTERFACE_ID_IDIAMOND_WRITABLE)).to.equal(true); expect(await lobby.supportsInterface(_INTERFACE_ID_IERC173)).to.equal(true); }); - it('new Lobby has correct ERC721 interfaces', async function () { - expect(await lobby.supportsInterface(_INTERFACE_ID_IERC721)).to.equal(true); - expect(await lobby.supportsInterface(_INTERFACE_ID_IERC721METADATA)).to.equal(true); - expect(await lobby.supportsInterface(_INTERFACE_ID_IERC721ENUMERABLE)).to.equal(true); + it('new Lobby has correct ERC1155 interfaces', async function () { + expect(await lobby.supportsInterface(_INTERFACE_ID_IERC1155)).to.equal(true); + expect(await lobby.supportsInterface(_INTERFACE_ID_IERC1155METADATA)).to.equal(true); + expect(await lobby.supportsInterface(_INTERFACE_ID_IERC1155ENUMERABLE)).to.equal(true); }); + it('test fallback', async function () { expect(await lobby.getFallbackAddress()).to.equal(ethers.constants.AddressZero); }); diff --git a/eth/test/DFMove.test.ts b/eth/test/DFMove.test.ts index e6cd9925..e368ddd7 100644 --- a/eth/test/DFMove.test.ts +++ b/eth/test/DFMove.test.ts @@ -1,11 +1,12 @@ -import { ArtifactType } from '@dfdao/types'; +import { ArtifactType, SpaceshipType } from '@dfdao/types'; import { loadFixture, time } from '@nomicfoundation/hardhat-network-helpers'; import { expect } from 'chai'; import { BigNumber } from 'ethers'; import { ethers } from 'hardhat'; import { conquerUnownedPlanet, - createArtifactOnPlanet, + createArtifact, + getArtifactsOnPlanet, increaseBlockchainTime, makeInitArgs, makeMoveArgs, @@ -51,7 +52,7 @@ describe('DarkForestMove', function () { }); it('allows controller to move ships to places they do not own with infinite distance', async function () { - const ship = (await world.user1Core.getArtifactsOnPlanet(SPAWN_PLANET_1.id))[0].artifact; + const ship = (await world.user1Core.getSpaceshipsOnPlanet(SPAWN_PLANET_1.id))[0]; const shipId = ship.id; await world.user1Core.move( @@ -60,15 +61,17 @@ describe('DarkForestMove', function () { await increaseBlockchainTime(); await world.user1Core.refreshPlanet(LVL2_PLANET_SPACE.id); - expect((await world.user1Core.getArtifactsOnPlanet(SPAWN_PLANET_1.id)).length).to.be.eq(4); - expect((await world.user1Core.getArtifactsOnPlanet(LVL2_PLANET_SPACE.id)).length).to.be.eq(1); + expect((await world.user1Core.getSpaceshipsOnPlanet(SPAWN_PLANET_1.id)).length).to.be.eq(4); + expect((await world.user1Core.getSpaceshipsOnPlanet(LVL2_PLANET_SPACE.id)).length).to.be.eq( + 1 + ); }); it('allows controller to move ships between their own planets', async function () { await conquerUnownedPlanet(world, world.user1Core, SPAWN_PLANET_1, LVL1_ASTEROID_NEBULA); await increaseBlockchainTime(); - const ship = (await world.user1Core.getArtifactsOnPlanet(SPAWN_PLANET_1.id))[0].artifact; + const ship = (await world.user1Core.getSpaceshipsOnPlanet(SPAWN_PLANET_1.id))[0]; const shipId = ship.id; await world.user1Core.move( ...makeMoveArgs(SPAWN_PLANET_1, LVL1_ASTEROID_NEBULA, 1000, 0, 0, shipId) @@ -77,51 +80,61 @@ describe('DarkForestMove', function () { await increaseBlockchainTime(); await world.user1Core.refreshPlanet(LVL1_ASTEROID_NEBULA.id); - expect((await world.user1Core.getArtifactsOnPlanet(LVL1_ASTEROID_NEBULA.id)).length).to.be.eq( - 1 - ); + expect( + (await world.user1Core.getSpaceshipsOnPlanet(LVL1_ASTEROID_NEBULA.id)).length + ).to.be.eq(1); }); it('should not allow you to move enemy ships on your own planet', async function () { - const ship = (await world.user2Core.getArtifactsOnPlanet(SPAWN_PLANET_2.id))[0].artifact; - const shipId = ship.id; + const user2shipId = (await world.user1Core.getSpaceshipsOnPlanet(SPAWN_PLANET_2.id))[0].id; await conquerUnownedPlanet(world, world.user1Core, SPAWN_PLANET_1, LVL2_PLANET_SPACE); await world.user2Core.move( - ...makeMoveArgs(SPAWN_PLANET_2, LVL2_PLANET_SPACE, 1000, 0, 0, shipId) + ...makeMoveArgs(SPAWN_PLANET_2, LVL2_PLANET_SPACE, 1000, 0, 0, user2shipId) ); await increaseBlockchainTime(); - await expect( - world.user1Core.move(...makeMoveArgs(LVL2_PLANET_SPACE, SPAWN_PLANET_2, 1000, 0, 0, shipId)) + world.user1Core.move( + ...makeMoveArgs(LVL2_PLANET_SPACE, SPAWN_PLANET_2, 1000, 0, 0, user2shipId) + ) ).to.be.revertedWith('you can only move your own ships'); }); it('should not consume a photoid if moving a ship off a planet with one activated', async function () { - const artifactId = await createArtifactOnPlanet( + await conquerUnownedPlanet(world, world.user1Core, SPAWN_PLANET_1, LVL2_PLANET_SPACE); + const artifactId = await createArtifact( world.contract, world.user1.address, SPAWN_PLANET_1, ArtifactType.PhotoidCannon ); - await world.user1Core.activateArtifact(SPAWN_PLANET_1.id, artifactId, 0); + await world.user1Core.activateArtifact(LVL2_PLANET_SPACE.id, artifactId, 0); + expect((await world.contract.getActiveArtifactOnPlanet(LVL2_PLANET_SPACE.id)).id).to.equal( + artifactId + ); + await increaseBlockchainTime(); - const ship = (await world.user1Core.getArtifactsOnPlanet(SPAWN_PLANET_1.id))[0].artifact; - const shipId = ship.id; + // Move + const ship = (await world.user1Core.getSpaceshipsOnPlanet(SPAWN_PLANET_1.id)).filter( + (s) => s.spaceshipType === SpaceshipType.ShipGear + )[0]; await world.user1Core.move( - ...makeMoveArgs(SPAWN_PLANET_1, LVL1_ASTEROID_1, 100, 0, 0, shipId) + ...makeMoveArgs(SPAWN_PLANET_1, LVL2_PLANET_SPACE, 100, 0, 0, ship?.id) ); - await world.contract.refreshPlanet(SPAWN_PLANET_1.id); - const activePhotoid = (await world.contract.getArtifactsOnPlanet(SPAWN_PLANET_1.id)).filter( - (a) => a.artifact.artifactType === ArtifactType.PhotoidCannon - )[0]; + await increaseBlockchainTime(); + await world.user1Core.move( + ...makeMoveArgs(LVL2_PLANET_SPACE, LVL1_ASTEROID_1, 100, 0, 0, ship?.id) + ); + + await world.contract.refreshPlanet(LVL2_PLANET_SPACE.id); + const activePhotoid = await getArtifactsOnPlanet(world, LVL2_PLANET_SPACE.id); // If the photoid is not there, it was used during ship move - expect(activePhotoid).to.not.eq(undefined); + expect(activePhotoid[0].id).to.equal(artifactId); }); }); @@ -918,10 +931,10 @@ describe('move rate limits', function () { await world.contract.adminGiveSpaceShip( SPAWN_PLANET_1.id, world.user1.address, - ArtifactType.ShipMothership + SpaceshipType.ShipMothership ); - const ship = (await world.user1Core.getArtifactsOnPlanet(SPAWN_PLANET_1.id))[0].artifact; + const ship = (await world.user1Core.getSpaceshipsOnPlanet(SPAWN_PLANET_1.id))[0]; await world.user1Core.move( ...makeMoveArgs(SPAWN_PLANET_1, LVL2_PLANET_SPACE, 10, 0, 0, ship.id) @@ -931,10 +944,10 @@ describe('move rate limits', function () { await world.contract.adminGiveSpaceShip( SPAWN_PLANET_1.id, world.user1.address, - ArtifactType.ShipMothership + SpaceshipType.ShipMothership ); - const ship = (await world.user1Core.getArtifactsOnPlanet(SPAWN_PLANET_1.id))[0].artifact; + const ship = (await world.user1Core.getSpaceshipsOnPlanet(SPAWN_PLANET_1.id))[0]; await expect( world.user1Core.move(...makeMoveArgs(SPAWN_PLANET_1, LVL2_PLANET_SPACE, 1000, 0, 0, ship.id)) @@ -943,7 +956,7 @@ describe('move rate limits', function () { await increaseBlockchainTime(); await world.user1Core.refreshPlanet(LVL2_PLANET_SPACE.id); - const numShipsOnPlanet = (await world.user1Core.getArtifactsOnPlanet(LVL2_PLANET_SPACE.id)) + const numShipsOnPlanet = (await world.user1Core.getSpaceshipsOnPlanet(LVL2_PLANET_SPACE.id)) .length; expect(numShipsOnPlanet).to.be.eq(6); @@ -957,10 +970,10 @@ describe('move rate limits', function () { await world.contract.adminGiveSpaceShip( SPAWN_PLANET_1.id, world.user1.address, - ArtifactType.ShipMothership + SpaceshipType.ShipMothership ); - const ship = (await world.user1Core.getArtifactsOnPlanet(SPAWN_PLANET_1.id))[0].artifact; + const ship = (await world.user1Core.getSpaceshipsOnPlanet(SPAWN_PLANET_1.id))[0]; await world.user1Core.move( ...makeMoveArgs(SPAWN_PLANET_1, LVL2_PLANET_SPACE, 10, 0, 0, ship.id) @@ -974,7 +987,7 @@ describe('move rate limits', function () { await increaseBlockchainTime(); await world.user1Core.refreshPlanet(LVL2_PLANET_SPACE.id); - const numShipsOnPlanet = (await world.user1Core.getArtifactsOnPlanet(LVL2_PLANET_SPACE.id)) + const numShipsOnPlanet = (await world.user1Core.getSpaceshipsOnPlanet(LVL2_PLANET_SPACE.id)) .length; expect(numShipsOnPlanet).to.be.eq(6); diff --git a/eth/test/DFScoringRound2.test.ts b/eth/test/DFSilver.ts similarity index 87% rename from eth/test/DFScoringRound2.test.ts rename to eth/test/DFSilver.ts index 79fffe5c..17a08413 100644 --- a/eth/test/DFScoringRound2.test.ts +++ b/eth/test/DFSilver.ts @@ -4,7 +4,7 @@ import { conquerUnownedPlanet, feedSilverToCap, makeInitArgs } from './utils/Tes import { defaultWorldFixture, World } from './utils/TestWorld'; import { LVL1_ASTEROID_1, LVL3_SPACETIME_1, SPAWN_PLANET_1 } from './utils/WorldConstants'; -describe('DFScoringRound2', async function () { +describe('DFSilver', async function () { // Bump the time out so that the test doesn't timeout during // initial fixture creation this.timeout(1000 * 60); @@ -42,7 +42,7 @@ describe('DFScoringRound2', async function () { // FIXME(blaine): This should have been done client-side because this type of // division isn't supposed to be done in the contract. That's the whole point of // `CONTRACT_PRECISION` - expect((await world.contract.players(world.user1.address)).score).to.equal( + expect(await world.contract.getSilverBalance(world.user1.address)).to.equal( withdrawnAmount.div(1000) ); }); @@ -54,7 +54,7 @@ describe('DFScoringRound2', async function () { world.user1Core.withdrawSilver(LVL3_SPACETIME_1.id, withdrawnAmount) ).to.be.revertedWith('tried to withdraw more silver than exists on planet'); - expect((await world.contract.players(world.user1.address)).score).to.equal(0); + expect(await world.contract.getSilverBalance(world.user1.address)).to.equal(0); }); it("doesn't allow player to withdraw silver from non-trading post", async function () { @@ -64,7 +64,7 @@ describe('DFScoringRound2', async function () { world.user1Core.withdrawSilver(LVL1_ASTEROID_1.id, withdrawnAmount) ).to.be.revertedWith('can only withdraw silver from trading posts'); - expect((await world.contract.players(world.user1.address)).score).to.equal(0); + expect(await world.contract.getSilverBalance(world.user1.address)).to.equal(0); }); it("doesn't allow player to withdraw silver from planet that is not theirs", async function () { @@ -74,7 +74,7 @@ describe('DFScoringRound2', async function () { world.user2Core.withdrawSilver(LVL3_SPACETIME_1.id, withdrawnAmount) ).to.be.revertedWith('you must own this planet'); - expect((await world.contract.players(world.user1.address)).score).to.equal(0); - expect((await world.contract.players(world.user2.address)).score).to.equal(0); + expect(await world.contract.getSilverBalance(world.user1.address)).to.equal(0); + expect(await world.contract.getSilverBalance(world.user2.address)).to.equal(0); }); }); diff --git a/eth/test/DFSpaceShips.test.ts b/eth/test/DFSpaceShips.test.ts index 3615d698..ea5eb516 100644 --- a/eth/test/DFSpaceShips.test.ts +++ b/eth/test/DFSpaceShips.test.ts @@ -1,8 +1,9 @@ -import { ArtifactType } from '@dfdao/types'; +import { PlanetType, SpaceshipType } from '@dfdao/types'; import { loadFixture } from '@nomicfoundation/hardhat-network-helpers'; import { expect } from 'chai'; import { conquerUnownedPlanet, + getSpaceshipOnPlanetByType, increaseBlockchainTime, makeInitArgs, makeMoveArgs, @@ -16,7 +17,7 @@ import { SPAWN_PLANET_2, } from './utils/WorldConstants'; -describe('Space Ships', function () { +describe('DarkForestSpaceShips', function () { let world: World; async function worldFixture() { @@ -39,7 +40,9 @@ describe('Space Ships', function () { describe('spawning your ships', function () { it('gives you 5 space ships', async function () { - expect((await world.user1Core.getArtifactsOnPlanet(SPAWN_PLANET_1.id)).length).to.be.equal(5); + expect((await world.user1Core.getSpaceshipsOnPlanet(SPAWN_PLANET_1.id)).length).to.be.equal( + 5 + ); }); it('can only be done once per player', async function () { @@ -57,13 +60,56 @@ describe('Space Ships', function () { }); }); + describe('ship transfers', function () { + it('cannot transfer your own spaceship', async function () { + const motherShip = await getSpaceshipOnPlanetByType( + world.contract, + SPAWN_PLANET_1.id, + SpaceshipType.ShipMothership + ); + // Player owns ship. + expect(await world.contract.balanceOf(world.user1.address, motherShip.id)).to.equal(1); + await expect( + world.user1Core.safeTransferFrom( + world.user1.address, + world.user2.address, + motherShip.id, + 1, + '0x00' + ) + ).to.be.revertedWith('player cannot transfer a Spaceship'); + }); + it('cannot transfer other players spaceship', async function () { + await world.user2Core.giveSpaceShips(SPAWN_PLANET_2.id); + + const motherShip = await getSpaceshipOnPlanetByType( + world.contract, + SPAWN_PLANET_2.id, + SpaceshipType.ShipMothership + ); + // Other Player owns ship. + expect(await world.contract.balanceOf(world.user2.address, motherShip.id)).to.equal(1); + await expect( + world.user1Core.safeTransferFrom( + world.user2.address, + world.user1.address, + motherShip.id, + 1, + '0x00' + ) + ).to.be.revertedWith('ERC1155: caller is not owner nor approved'); + }); + }); + describe('using the Titan', async function () { this.timeout(0); it('pauses energy regeneration on planets', async function () { - const titan = (await world.user1Core.getArtifactsOnPlanet(SPAWN_PLANET_1.id)).find( - (a) => a.artifact.artifactType === ArtifactType.ShipTitan - )?.artifact; + const titan = await getSpaceshipOnPlanetByType( + world.contract, + SPAWN_PLANET_1.id, + SpaceshipType.ShipTitan + ); // Move Titan to planet await world.user1Core.move( @@ -106,6 +152,47 @@ describe('Space Ships', function () { }); }); + describe('using the Crescent', function () { + it('turns planet into an asteroid and burns crescent', async function () { + const crescent = await getSpaceshipOnPlanetByType( + world.contract, + SPAWN_PLANET_1.id, + SpaceshipType.ShipCrescent + ); + + // Move Crescent to planet + await world.user1Core.move( + ...makeMoveArgs(SPAWN_PLANET_1, LVL1_PLANET_DEEP_SPACE, 1000, 0, 0, crescent.id) + ); + + await increaseBlockchainTime(); + await world.contract.refreshPlanet(LVL1_PLANET_DEEP_SPACE.id); + + const crescentNewLocId = ( + await world.contract.getSpaceshipsOnPlanet(LVL1_PLANET_DEEP_SPACE.id) + )[0].id; + expect(crescentNewLocId).to.equal(crescent?.id); + + const planetBeforeActivate = await world.contract.planets(LVL1_PLANET_DEEP_SPACE.id); + await world.user1Core.activateArtifact(LVL1_PLANET_DEEP_SPACE.id, crescent.id, 0); + const planetAfterActivate = await world.contract.planets(LVL1_PLANET_DEEP_SPACE.id); + // Silver is higher + expect(planetBeforeActivate.silverGrowth).to.be.lessThan(planetAfterActivate.silverGrowth); + // Crescent is no longer on planet. + expect( + (await world.contract.getSpaceshipsOnPlanet(LVL1_PLANET_DEEP_SPACE.id)).length + ).to.equal(0); + // Planet was planet + expect(planetBeforeActivate.planetType).to.equal(PlanetType.PLANET); + // Planet is now asteroid. + expect(planetAfterActivate.planetType).to.equal(PlanetType.SILVER_MINE); + // Cannot activate again. + await expect( + world.user1Core.activateArtifact(LVL1_PLANET_DEEP_SPACE.id, crescent.id, 0) + ).to.be.revertedWith('you can only activate artifacts you own or on planet'); + }); + }); + describe('spawning on non-home planet', async function () { let world: World; diff --git a/eth/test/utils/TestUtils.ts b/eth/test/utils/TestUtils.ts index ac996184..f9bd0c6b 100644 --- a/eth/test/utils/TestUtils.ts +++ b/eth/test/utils/TestUtils.ts @@ -1,4 +1,5 @@ import type { DarkForest } from '@dfdao/contracts/typechain'; +import { ArtifactStructOutput } from '@dfdao/contracts/typechain/hardhat-diamond-abi/HardhatDiamondABI.sol/DarkForest'; import { modPBigInt } from '@dfdao/hashing'; import { buildContractCallArgs, @@ -7,17 +8,27 @@ import { WhitelistSnarkInput, } from '@dfdao/snarks'; import { whitelistSnarkWasmPath, whitelistSnarkZkeyPath } from '@dfdao/snarks/node'; -import { ArtifactRarity, ArtifactType, Biome } from '@dfdao/types'; +import { + ArtifactRarity, + ArtifactRarityNames, + ArtifactType, + ArtifactTypeNames, + Biome, + BiomeNames, + SpaceshipType, + TokenTypeNames, +} from '@dfdao/types'; import { bigIntFromKey } from '@dfdao/whitelist'; import { mine, time } from '@nomicfoundation/hardhat-network-helpers'; import bigInt from 'big-integer'; +import { expect } from 'chai'; import { BigNumber, BigNumberish, constants } from 'ethers'; +import hre from 'hardhat'; // @ts-ignore import * as snarkjs from 'snarkjs'; import { TestLocation } from './TestLocation'; import { World } from './TestWorld'; import { ARTIFACT_PLANET_1, initializers, LARGE_INTERVAL } from './WorldConstants'; - const { PLANETHASH_KEY, SPACETYPE_KEY, @@ -34,6 +45,14 @@ export function hexToBigNumber(hex: string): BigNumber { return BigNumber.from(`0x${hex}`); } +export function prettyPrintToken(token: ArtifactStructOutput) { + console.log( + `~Token~\nID: ${token.id}\nCollection: ${TokenTypeNames[token.tokenType]}\nRarity: ${ + ArtifactRarityNames[token.rarity] + }\nType: ${ArtifactTypeNames[token.artifactType]}\nBiome: ${BiomeNames[token.planetBiome]}` + ); +} + export function makeRevealArgs( planetLoc: TestLocation, x: number, @@ -290,9 +309,10 @@ export async function user1MintArtifactPlanet(user1Core: DarkForest) { await increaseBlockchainTime(); const findArtifactTx = await user1Core.findArtifact(...makeFindArtifactArgs(ARTIFACT_PLANET_1)); const findArtifactReceipt = await findArtifactTx.wait(); - // 0th event is erc721 transfer (i think); 1st event is UpdateArtifact, 2nd argument of this event is artifactId - const artifactId = findArtifactReceipt.events?.[1].args?.[1]; - return artifactId; + // 0th event is erc721 transfer (i think); 1st event is UpdateArtifact, 2nd argument of this event + // is artifactId + const artifactId = findArtifactReceipt.events?.[1].args?.artifactId; + return artifactId as BigNumber; } export async function getArtifactsOwnedBy(contract: DarkForest, addr: string) { @@ -302,18 +322,25 @@ export async function getArtifactsOwnedBy(contract: DarkForest, addr: string) { ); } -export async function createArtifactOnPlanet( +// Gets Artifacts but not Spaceships +export async function getArtifactsOnPlanet(world: World, locationId: BigNumberish) { + return (await world.contract.getArtifactsOnPlanet(locationId)).filter( + (artifact) => artifact.artifactType < ArtifactType.ShipMothership + ); +} + +export async function createArtifact( contract: DarkForest, owner: string, planet: TestLocation, - type: ArtifactType, - { rarity, biome }: { rarity?: ArtifactRarity; biome?: Biome } = {} + artifactType: ArtifactType, + rarity?: ArtifactRarity, + biome?: Biome ) { rarity ||= ArtifactRarity.Common; biome ||= Biome.FOREST; - const tokenId = hexToBigNumber(Math.floor(Math.random() * 10000000000).toString(16)); - + const tokenId = await contract.createArtifactId(rarity, artifactType, biome); await contract.adminGiveArtifact({ tokenId, discoverer: owner, @@ -321,9 +348,48 @@ export async function createArtifactOnPlanet( planetId: planet.id, rarity: rarity.toString(), biome: biome.toString(), - artifactType: type.toString(), + artifactType: artifactType.toString(), controller: ZERO_ADDRESS, }); return tokenId; } + +export async function testDeactivate(world: World, locationId: BigNumberish) { + expect((await getArtifactsOnPlanet(world, locationId)).length).to.equal(0); + expect(await world.contract.hasActiveArtifact(locationId)).to.equal(false); + expect(await world.contract.getArtifactActivationTimeOnPlanet(locationId)).to.equal(0); +} + +export async function activateAndConfirm( + contract: DarkForest, + locationId: BigNumber, + tokenId: BigNumberish, + wormHoleTo?: BigNumberish +) { + const activateTx = await contract.activateArtifact(locationId, tokenId, wormHoleTo || 0); + const activateRct = await activateTx.wait(); + const block = await hre.ethers.provider.getBlock(activateRct.blockNumber); + expect(await contract.getArtifactActivationTimeOnPlanet(locationId)).to.equal(block.timestamp); + expect((await contract.getActiveArtifactOnPlanet(locationId)).id).to.equal(tokenId); +} + +export async function getArtifactOnPlanetByType( + contract: DarkForest, + locationId: BigNumber, + artifactType: ArtifactType +) { + return (await contract.getArtifactsOnPlanet(locationId)).filter( + (artifact) => (artifact.artifactType as ArtifactType) === artifactType + )[0]; +} + +export async function getSpaceshipOnPlanetByType( + contract: DarkForest, + locationId: BigNumber, + shipType: SpaceshipType +) { + return (await contract.getSpaceshipsOnPlanet(locationId)).filter( + (s) => (s.spaceshipType as SpaceshipType) === shipType + )[0]; +} diff --git a/eth/test/utils/WorldConstants.ts b/eth/test/utils/WorldConstants.ts index 9ae8db2d..d211bf42 100644 --- a/eth/test/utils/WorldConstants.ts +++ b/eth/test/utils/WorldConstants.ts @@ -26,7 +26,7 @@ const defaultInitializerValues = { PLANET_LEVEL_THRESHOLDS: [16777216, 4194292, 1048561, 262128, 65520, 16368, 4080, 1008, 240, 48], PLANET_RARITY: 16384, PLANET_TRANSFER_ENABLED: true, - PHOTOID_ACTIVATION_DELAY: 60 * 60 * 12, + PHOTOID_ACTIVATION_DELAY: 60 * 60 * 12, // 12 hours SPAWN_RIM_AREA: 7234560000, LOCATION_REVEAL_COOLDOWN: 60 * 60 * 24, PLANET_TYPE_WEIGHTS: [ @@ -391,6 +391,12 @@ export const ADMIN_PLANET = new TestLocation({ distFromOrigin: 1998, }); +export const ZERO_PLANET = new TestLocation({ + hex: '0000000000000000000000000000000000000000000000000000000000000069', + perlin: NEBULA_PERLIN, + distFromOrigin: 1998, +}); + // not under difficulty threshold export const ADMIN_PLANET_CLOAKED = new TestLocation({ hex: '0100000000000000000000000000000000000000000000000000000000000069', diff --git a/eth/utils/diamond.ts b/eth/utils/diamond.ts index ce8d589b..80664ea5 100644 --- a/eth/utils/diamond.ts +++ b/eth/utils/diamond.ts @@ -41,7 +41,7 @@ export function toSignature(abiElement: unknown): string { const signaturesToIgnore = [ // The SolidState contracts adds a `supportsInterface` function, // but we already provide that function through DiamondLoupeFacet - ['DFArtifactFacet$', 'supportsInterface(bytes4)'], + ['DFTokenFacet$', 'supportsInterface(bytes4)'], ] as const; const eventSignatures = new Set(); diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 289d1025..776e7a94 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -42,6 +42,8 @@ export * from './plugin'; export * from './renderer'; export * from './reveal'; export * from './setting'; +export * from './spaceship'; +export * from './token'; export * from './transaction'; export * from './transactions'; export * from './upgrade'; diff --git a/packages/types/src/spaceship.ts b/packages/types/src/spaceship.ts new file mode 100644 index 00000000..027df98a --- /dev/null +++ b/packages/types/src/spaceship.ts @@ -0,0 +1,32 @@ +import { Abstract } from './utility'; + +/** + * Abstract type representing an artifact type. + */ +export type SpaceshipType = Abstract; + +/** + * Enumeration of artifact types. + */ +export const SpaceshipType = { + Unknown: 0 as SpaceshipType, + ShipMothership: 1 as SpaceshipType, + ShipCrescent: 2 as SpaceshipType, + ShipWhale: 3 as SpaceshipType, + ShipGear: 4 as SpaceshipType, + ShipTitan: 5 as SpaceshipType, + + // Don't forget to update MIN_ARTIFACT_TYPE and/or MAX_ARTIFACT_TYPE in the `constants` package +} as const; + +/** + * Mapping from SpaceshipType to pretty-printed names. + */ +export const SpaceshipTypeNames = { + [SpaceshipType.Unknown]: 'Unknown', + [SpaceshipType.ShipMothership]: 'Mothership', + [SpaceshipType.ShipCrescent]: 'Crescent', + [SpaceshipType.ShipWhale]: 'Whale', + [SpaceshipType.ShipGear]: 'Gear', + [SpaceshipType.ShipTitan]: 'Titan', +} as const; diff --git a/packages/types/src/token.ts b/packages/types/src/token.ts new file mode 100644 index 00000000..0096ec49 --- /dev/null +++ b/packages/types/src/token.ts @@ -0,0 +1,18 @@ +import { Abstract } from './utility'; + +export type TokenType = Abstract; + +export const TokenType = { + Unknown: 0 as TokenType, + Artifact: 1 as TokenType, + Spaceship: 2 as TokenType, +} as const; + +/** + * Mapping from TokenType to pretty-printed names. + */ +export const TokenTypeNames = { + [TokenType.Unknown]: 'Unknown', + [TokenType.Artifact]: 'Artifact', + [TokenType.Spaceship]: 'Spaceship', +} as const;