-
Notifications
You must be signed in to change notification settings - Fork 39
/
Inventory.sol
359 lines (280 loc) · 13.5 KB
/
Inventory.sol
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
// SPDX-License-Identifier: BSD-3-Clause-Clear
pragma solidity ^0.8.0;
import {CardsCollection} from "./CardsCollection.sol";
import {DeckAirdrop} from "./DeckAirdrop.sol";
import {Game} from "./Game.sol";
import {InventoryCardsCollection} from "./InventoryCardsCollection.sol";
import {Utils} from "./libraries/Utils.sol";
import {Ownable} from "openzeppelin/access/Ownable.sol";
contract Inventory is Ownable {
// =============================================================================================
// ERRORS
// Deck size is below MIN_DECK_SIZE.
error SmallDeckEnergy();
// Deck size exceeds MAX_DECK_SIZE.
error BigDeckEnergy();
// The user has attributed all consecutive deck IDs. However, it is possible that there exists
// empty deck IDs that can be used via `replaceDeck`.
error OutOfDeckIDs();
// Using an unknown deck ID.
error DeckDoesNotExist(address player, uint8 deckID);
// Trying to perform an action on behalf of a player without delegation.
error PlayerNotDelegatedToSender();
// A deck contains a card that the player hasn't transferred to the inventory.
error CardNotInInventory(uint256 cardID);
// A player cannot add/remove cards to/from the inventory, or modify decks, while participating
// in a game.
error PlayerIsInActiveGame(address player);
// Number of card copies in a deck cannot exceed `MAX_CARD_COPY`.
error CardExceedsMaxCopy(uint256 cardID);
// =============================================================================================
// EVENTS
// A player added a card to the inventory.
event CardAdded(address indexed player, uint256 indexed cardID);
// A player removed a card from the inventory.
event CardRemoved(address indexed player, uint256 indexed cardID);
event DeckAdded(address indexed player, uint8 deckID);
event DeckRemoved(address indexed player, uint8 indexed deckID);
event CardAddedToDeck(uint8 indexed deckID, uint256 indexed cardID);
event CardRemovedFromDeck(uint8 indexed deckID, uint256 indexed cardID);
// =============================================================================================
// CONSTANTS
// Max number of decks that each player can have.
uint256 public constant MAX_DECKS = 256;
// Min number of cards in a deck.
uint256 public constant MIN_DECK_SIZE = 10;
// Max number of cards in a deck.
uint256 public constant MAX_DECK_SIZE = 62;
// Max card copies in a deck.
uint256 private constant MAX_CARD_COPY = 3;
// =============================================================================================
// TYPES
// We need a struct because Solidity is unable to copy an array from memory to storage
// directly, but can do it when the array is embedded in a struct.
struct Deck {
uint256[] cards;
}
// =============================================================================================
// FIELDS
// Maps a player to list of their decks.
mapping(address => Deck[]) private decks;
// Maps keccak256(delegate, player) to true if the player delegated to the delegate.
mapping(bytes32 => bool) public delegations;
// The NFT collection that contains all admissible cards for use with this inventory contract.
// This can't be called `cardsCollection` because it would cause a naming conflict with
// the InventoryCardsCollection contract when generating hooks with wagmi.
CardsCollection public originalCardsCollection;
// The NFT collection that contains soulbound tokens matching cards transferred to the inventory
// by a player.
InventoryCardsCollection public inventoryCardsCollection;
// Airdrop manager.
address public airdrop;
// Game contract.
Game public game;
// =============================================================================================
// MODIFIERS
// Checks that the message sender has a deck with the given ID.
modifier exists(address player, uint8 deckID) {
if (deckID >= decks[player].length || decks[player][deckID].cards.length == 0) {
revert DeckDoesNotExist(player, deckID);
}
_;
}
// ---------------------------------------------------------------------------------------------
// Checks that the player has delegated to the message sender.
modifier delegated(address player) {
if (
msg.sender != player && msg.sender != airdrop
&& !delegations[keccak256(abi.encodePacked(msg.sender, player))]
) {
revert PlayerNotDelegatedToSender();
}
_;
}
// ---------------------------------------------------------------------------------------------
// Checks that the player is not currently participating in any game.
modifier notInGame(address player) {
if (game.playerActive(player)) revert PlayerIsInActiveGame(player);
_;
}
// =============================================================================================
// INITIALIZATION
// deploySalt is the CREATE2 salt used to deplay the InventoryCardsCollection.
constructor(bytes32 deploySalt, CardsCollection cardsCollection_) Ownable() {
originalCardsCollection = cardsCollection_;
inventoryCardsCollection = new InventoryCardsCollection{salt: deploySalt}(cardsCollection_);
}
// ---------------------------------------------------------------------------------------------
function setAirdrop(DeckAirdrop airdrop_) external onlyOwner {
airdrop = address(airdrop_);
}
// ---------------------------------------------------------------------------------------------
function setGame(Game game_) external onlyOwner {
game = game_;
}
// =============================================================================================
// FUNCTIONS
// Lets the delegate perform actions on behalf of the message sender (player).
function setDelegation(address delegate, bool isDelegated) external {
delegations[keccak256(abi.encodePacked(delegate, msg.sender))] = isDelegated;
}
// ---------------------------------------------------------------------------------------------
// Transfers a card of the sender to the inventory, mints a soulbound inventory card to the
// sender in return.
function addCard(address player, uint256 cardID) external delegated(player) {
originalCardsCollection.transferFrom(player, address(this), cardID);
inventoryCardsCollection.mint(player, cardID);
emit CardAdded(player, cardID);
}
// ---------------------------------------------------------------------------------------------
// Burns the sender's inventory card and transfers the card back to him.
function removeCard(address player, uint256 cardID) external delegated(player) notInGame(player) {
inventoryCardsCollection.burn(cardID);
originalCardsCollection.transferFrom(address(this), player, cardID);
emit CardRemoved(player, cardID);
}
// ---------------------------------------------------------------------------------------------
function checkDeckSize(Deck storage deck) internal view {
if (deck.cards.length > MAX_DECK_SIZE) {
revert BigDeckEnergy();
}
}
// ---------------------------------------------------------------------------------------------
function _addDeck(address player, uint8 deckID, Deck calldata deck) internal {
if (deck.cards.length < MIN_DECK_SIZE) {
revert SmallDeckEnergy();
}
if (deck.cards.length > MAX_DECK_SIZE) {
revert BigDeckEnergy();
}
decks[player][deckID] = deck;
}
// ---------------------------------------------------------------------------------------------
// Adds a new deck with the given cards for the sender. The player does not need to have the
// cards in the inventory to do this (however, if he does not, the deck will not be playable).
function addDeck(address player, Deck calldata deck) external delegated(player) returns (uint8 deckID) {
uint256 longDeckID = decks[player].length;
if (longDeckID >= MAX_DECKS) {
revert OutOfDeckIDs();
}
deckID = uint8(longDeckID);
decks[player].push();
_addDeck(player, deckID, deck);
emit DeckAdded(player, deckID);
}
// ---------------------------------------------------------------------------------------------
// Remove the given deck for the sender, leaving the deck at the given ID empty.
function removeDeck(address player, uint8 deckID)
external
delegated(player)
exists(player, deckID)
notInGame(player)
{
delete decks[player][deckID];
emit DeckRemoved(player, deckID);
}
// ---------------------------------------------------------------------------------------------
// Replace the deck with the given ID. This can be a deck that was previously removed, granted
// that there exists a deck with a higher ID. Emits events for deck removal and adding.
function replaceDeck(address player, uint8 deckID, Deck calldata deck)
external
delegated(player)
exists(player, deckID)
notInGame(player)
{
_addDeck(player, deckID, deck);
emit DeckRemoved(player, deckID);
emit DeckAdded(player, deckID);
}
// ---------------------------------------------------------------------------------------------
// Add the given card to the given deck. The player does not need to have the card in the
// inventory to do this (however, if he does not, the deck will not be playable).
// You can't remove a card from a deck if it would bring the size to above the maximum size.
function addCardToDeck(address player, uint8 deckID, uint256 cardID)
external
delegated(player)
exists(player, deckID)
notInGame(player)
{
Deck storage deck = decks[player][deckID];
if (deck.cards.length == MAX_DECK_SIZE) {
revert BigDeckEnergy();
}
deck.cards.push(cardID);
emit CardAddedToDeck(deckID, cardID);
}
// ---------------------------------------------------------------------------------------------
// Remove the card a the given index in the given deck.
// You can't remove a card from a deck if it would bring the size to below the minimum size.
function removeCardFromDeck(address player, uint8 deckID, uint8 index)
external
delegated(player)
exists(player, deckID)
notInGame(player)
{
Deck storage deck = decks[player][deckID];
if (deck.cards.length == MIN_DECK_SIZE) {
revert BigDeckEnergy();
}
uint256 cardID = deck.cards[index];
deck.cards[index] = deck.cards[deck.cards.length - 1];
deck.cards.pop();
emit CardRemovedFromDeck(deckID, cardID);
}
// ---------------------------------------------------------------------------------------------
// Checks that the player has all the cards in the given deck in the inventory.
function checkDeck(address player, uint8 deckID) external view exists(player, deckID) {
Deck memory deck = decks[player][deckID];
// Cache deck length.
uint256 deckLength = deck.cards.length;
for (uint256 i = 0; i < deckLength; ++i) {
uint256 cardID = deck.cards[i];
if (inventoryCardsCollection.ownerOf(cardID) != player) {
revert CardNotInInventory(cardID);
}
}
// Sort cards according to type.
uint256[] memory sortedCards = Utils.sort(getCardTypes(deck.cards));
uint256 cardCopies;
uint256 prevCardType;
for (uint256 i = 0; i < deckLength; ++i) {
uint256 cardType = sortedCards[i];
if (cardType == prevCardType) {
cardCopies++;
// check that each card does not exceed its maximum amount of copies.
if (cardCopies > MAX_CARD_COPY) {
revert CardExceedsMaxCopy(cardType);
}
} else {
cardCopies = uint256(1);
prevCardType = sortedCards[i];
}
}
// NOTE(norswap): Deck size is implicitly checked when updating the deck.
}
// ---------------------------------------------------------------------------------------------
// Returns the list of cards in the given deck of the given player.
function getDeck(address player, uint8 deckID)
external
view
exists(player, deckID)
returns (uint256[] memory deckCards)
{
return decks[player][deckID].cards;
}
// ---------------------------------------------------------------------------------------------
// Returns the number of deck a player has created.
function getNumDecks(address player) external view returns (uint8) {
return uint8(decks[player].length);
}
// ---------------------------------------------------------------------------------------------
function getCardTypes(uint256[] memory cardIDArr) public view returns (uint256[] memory) {
uint256 len = cardIDArr.length;
uint256[] memory cardTypeArr = new uint256[](len);
for (uint256 i = 0; i < len; i++) {
cardTypeArr[i] = uint256(originalCardsCollection.cardType(cardIDArr[i]));
}
return cardTypeArr;
}
// ---------------------------------------------------------------------------------------------
}