diff --git a/src/gameEvents.js b/src/gameEvents.js index 4e09880..e18b7fc 100644 --- a/src/gameEvents.js +++ b/src/gameEvents.js @@ -8,3 +8,4 @@ export const RESTART_LEVEL = 'r'; export const START_NEXT_LEVEL = 'snl'; export const ARCADIAN_HEAD_SELECTED = 'ahs'; export const NEAR_TOKENS_ADDED = 'n'; +export const NFT_MINT = 'nm'; diff --git a/src/index.js b/src/index.js index 5c91e6c..bd4c673 100644 --- a/src/index.js +++ b/src/index.js @@ -34,12 +34,14 @@ const initNear = () => { const promises = [ nearConnection.nft_tokens_for_owner(nearConnection.accountId), nearConnection.nft_tokens_by_series(HANG_BY_A_THREAD_SERIES_TESTNET), + nearConnection.getNftCollections(), ]; - Promise.all(promises).then(([tokensForOwner, tokensBySeries]) => { - setNftTokens(tokensForOwner, tokensBySeries); - console.log('nft_tokens_for_owner', tokensForOwner); - console.log('nft_tokens_by_series', tokensBySeries); - }); + + Promise.all(promises).then( + ([tokensForOwner, tokensBySeries, collections]) => { + setNftTokens(tokensForOwner, tokensBySeries, collections); + } + ); }); }); }; diff --git a/src/menu.js b/src/menu.js index d10f1d1..7d80520 100644 --- a/src/menu.js +++ b/src/menu.js @@ -5,6 +5,7 @@ import { emit, on } from 'kontra'; import { LEVEL_COMPLETE, NEAR_TOKENS_ADDED, + NFT_MINT, RESTART_LEVEL, START_LEVEL, START_NEXT_LEVEL, @@ -12,6 +13,7 @@ import { import { fetchArcadianHeads } from './arcadianApi'; import { nftTokensBySeries, setSelectedArcadian } from './store'; import { IPFS_BASE_PATH } from './near/nearConnection'; +import { doesOwnNft, getNearLevelId } from './utils'; const overlayIds = ['main', 'bonus', 'levels', 'level-dialog', 'near-levels']; const levels = 20; @@ -38,16 +40,38 @@ const initLevels = () => { } }; -const initNearLevels = ({ nftTokensBySeries, nftTokensForOwner }) => { +const initNearLevels = ({ + nftTokensBySeries, + nftTokensForOwner, + nftCollections, +}) => { const nearLoadingEl = document.getElementById('loadingNearLevels'); if (nearLoadingEl) nearLoadingEl.remove(); const levelsGridEl = document.getElementById('near-levels-grid'); - nftTokensBySeries.forEach((l) => { + nftCollections.forEach((collection) => { const levelEl = document.createElement('button'); const imgEl = document.createElement('img'); - imgEl.setAttribute('src', IPFS_BASE_PATH + l.metadata.media); - levelEl.textContent = l.metadata.title; + const ownsNft = doesOwnNft(collection.token_series_id, nftTokensForOwner); + + imgEl.setAttribute('src', IPFS_BASE_PATH + collection.metadata.media); + levelEl.setAttribute('near', 'true'); + if (!ownsNft) { + levelEl.setAttribute('disabled', !ownsNft); + } + levelEl.setAttribute('token-series-id', collection.token_series_id); + levelEl.setAttribute('price', collection.price); + levelEl.textContent = collection.metadata.title; levelEl.appendChild(imgEl); + const mintForPriceEl = document.createElement('span'); + if (!ownsNft) { + mintForPriceEl.textContent = + 'Buy level for: ' + + (collection.price / Math.pow(10, 24)).toFixed(2) + + ' Ⓝ'; + } else { + mintForPriceEl.textContent = 'Click to play level'; + } + levelEl.appendChild(mintForPriceEl); levelEl.classList.add('level-item'); levelsGridEl.appendChild(levelEl); }); @@ -131,13 +155,31 @@ const onContainerClick = (e) => { if (classList.contains('close-icon')) { showOverlay('main'); + return; } - if (classList.contains('level-item')) { + + const btn = e.target.closest('button'); + if (btn && btn.getAttribute('near')) { + onNearLevelClick(btn); + } else if (classList.contains('level-item')) { showOverlay(); emit(START_LEVEL, { levelId: +e.target.textContent }); } }; +const onNearLevelClick = (btn) => { + if (btn && btn.getAttribute('disabled') === 'true') { + const token_series_id = btn.getAttribute('token-series-id'); + const priceInYoctoNear = btn.getAttribute('price'); + emit(NFT_MINT, { token_series_id, priceInYoctoNear }); + } else { + showOverlay(); + emit(START_LEVEL, { + levelId: getNearLevelId(btn.getAttribute('token-series-id')), + }); + } +}; + const showOverlay = (id) => { overlayIds.forEach((o) => { const overlayEl = document.getElementById(o); @@ -158,6 +200,10 @@ const onLevelComplete = () => { showOverlay('level-dialog'); document.getElementById('nextBtn').focus(); }; -const onNearTokensAdded = ({ nftTokensBySeries, nftTokensForOwner }) => { - initNearLevels({ nftTokensBySeries, nftTokensForOwner }); +const onNearTokensAdded = ({ + nftTokensBySeries, + nftTokensForOwner, + nftCollections, +}) => { + initNearLevels({ nftTokensBySeries, nftTokensForOwner, nftCollections }); }; diff --git a/src/near/nearConnection.js b/src/near/nearConnection.js index 2622864..b693198 100644 --- a/src/near/nearConnection.js +++ b/src/near/nearConnection.js @@ -1,3 +1,5 @@ +import { on } from 'kontra'; +import { NFT_MINT } from '../gameEvents'; import getConfig from './config'; export const HANG_BY_A_THREAD_SERIES_TESTNET = '2036'; @@ -5,6 +7,8 @@ export const MIRRORS_SERIES_TESTNET = '494'; export const PARAS_BASE_PATH_TESTNET = 'https://testnet.paras.id/token/paras-token-v2.testnet::'; export const IPFS_BASE_PATH = 'https://ipfs.fleek.co/ipfs/'; +export const PARAS_COLLECTION_API = + 'https://api-v3-marketplace-testnet.paras.id/token-series?collection_id=hang-by-a-thread-by-johnonymtestnet'; export class NearConnection { walletConnection; @@ -16,6 +20,7 @@ export class NearConnection { resolveContract; constructor() { + this.listenForGameEvents(); this.ready = new Promise((resolve, reject) => { this.resolveContract = resolve; }); @@ -42,11 +47,10 @@ export class NearConnection { // View methods are read only. They don't modify the state, but usually return some value. viewMethods: ['nft_tokens_for_owner', 'nft_tokens_by_series'], // Change methods can modify the state. But you don't receive the returned value when called. - changeMethods: ['setGreeting', 'setScore', 'setName'], + changeMethods: ['nft_mint'], } ); this.resolveContract(); - return this.walletConnection; } @@ -69,4 +73,27 @@ export class NearConnection { nft_tokens_by_series(token_series_id) { return this.contract.nft_tokens_by_series({ token_series_id }); } + nft_mint({ token_series_id, priceInYoctoNear }) { + return this.contract.nft_mint( + { + owner_id: this.accountId, + receiver_id: this.accountId, + token_series_id, + }, + '300000000000000', + priceInYoctoNear + ); + } + getNftCollections() { + return fetch(PARAS_COLLECTION_API) + .then((res) => res.json()) + .then((res) => { + return res.data.results.filter((data) => data.metadata.copies > 0); + }); + } + listenForGameEvents() { + on(NFT_MINT, ({ token_series_id, priceInYoctoNear }) => + this.nft_mint({ token_series_id, priceInYoctoNear }) + ); + } } diff --git a/src/store.js b/src/store.js index a1cfb9f..bde2fdb 100644 --- a/src/store.js +++ b/src/store.js @@ -7,6 +7,7 @@ const arcadianData = {}; let selectedArcadian = {}; export let nftTokensBySeries = []; export let nftTokensForOwner = []; +export let nftCollections = []; export const setGameWidth = (width) => { gameWidth = width; @@ -29,8 +30,13 @@ export const getSelectedArcadian = () => { return selectedArcadian; }; -export const setNftTokens = (tokensForOwner, tokensBySeries) => { +export const setNftTokens = (tokensForOwner, tokensBySeries, collections) => { nftTokensForOwner = tokensForOwner; nftTokensBySeries = tokensBySeries; - emit(NEAR_TOKENS_ADDED, { nftTokensBySeries, nftTokensForOwner }); + nftCollections = collections; + emit(NEAR_TOKENS_ADDED, { + nftTokensBySeries, + nftTokensForOwner, + nftCollections, + }); }; diff --git a/src/styles.css b/src/styles.css index cafb1c7..4c2e073 100644 --- a/src/styles.css +++ b/src/styles.css @@ -73,6 +73,12 @@ button:disabled { color: darkgray; cursor: not-allowed; } +button:disabled img { + filter: contrast(0.5); +} +button:hover img { + filter: none; +} .overlay { position: absolute; width: 100%; @@ -135,6 +141,7 @@ button:disabled { flex-flow: column; width: 250px; height: 300px; + cursor: pointer; } #bonus-grid { diff --git a/src/utils.js b/src/utils.js index cbb2955..14a83f2 100644 --- a/src/utils.js +++ b/src/utils.js @@ -25,3 +25,19 @@ export const lineIntersection = (p1, p2, p3, p4) => { let intersection = Vector(intersectionX, intersectionY); return intersection; }; + +export const doesOwnNft = (seriesId, nftTokensForOwner) => { + const token = nftTokensForOwner.find( + (token) => token.token_id.split(':')[0] === seriesId + ); + return !!token; +}; + +export const getNearLevelId = (tokenSeriesId) => { + switch (tokenSeriesId) { + case '2037': + return 2; + case '2036': + return 3; + } +};