Skip to content

Commit

Permalink
Merge branch 'koni/dev/issue-task-onchain' into telegram-dev-01
Browse files Browse the repository at this point in the history
  • Loading branch information
anhnhu committed May 14, 2024
2 parents d3aa682 + 2c8da6b commit de4d9c8
Show file tree
Hide file tree
Showing 11 changed files with 534 additions and 18 deletions.
87 changes: 77 additions & 10 deletions packages/extension-koni-ui/src/Popup/Home/Games/index.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
// Copyright 2019-2022 @subwallet/extension-ui authors & contributors
// SPDX-License-Identifier: Apache-2.0

import { ShopModal } from '@subwallet/extension-koni-ui/components';
import GameAccount from '@subwallet/extension-koni-ui/components/Games/GameAccount';
import GameEnergy from '@subwallet/extension-koni-ui/components/Games/GameEnergy';
import { ShopModalId } from '@subwallet/extension-koni-ui/components/Modal/Shop/ShopModal';
import { BookaSdk } from '@subwallet/extension-koni-ui/connector/booka/sdk';
import { Game } from '@subwallet/extension-koni-ui/connector/booka/types';
import { EnergyConfig, Game, GameInventoryItem, GameItem } from '@subwallet/extension-koni-ui/connector/booka/types';
import { useSetCurrentPage, useTranslation } from '@subwallet/extension-koni-ui/hooks';
import { GameApp } from '@subwallet/extension-koni-ui/Popup/Home/Games/gameSDK';
import { ThemeProps } from '@subwallet/extension-koni-ui/types';
import { Button, Image, Typography } from '@subwallet/react-ui';
import { Button, Icon, Image, ModalContext, Typography } from '@subwallet/react-ui';
import CN from 'classnames';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { ShoppingBag } from 'phosphor-react';
import React, { useCallback, useContext, useEffect, useRef, useState } from 'react';
import styled from 'styled-components';

type Props = ThemeProps;
Expand All @@ -28,10 +31,17 @@ function checkComingSoon (game: Game): boolean {
return gameStartTime > Date.now();
}

const shopModalId = ShopModalId;

const Component = ({ className }: Props): React.ReactElement => {
useSetCurrentPage('/home/games');
const gameIframe = useRef<HTMLIFrameElement>(null);
const [gameList, setGameList] = useState<Game[]>(apiSDK.gameList);
const [energyConfig, setEnergyConfig] = useState<EnergyConfig | undefined>(apiSDK.energyConfig);
const [gameItemMap, setGameItemMap] = useState<Record<string, GameItem[]>>(apiSDK.gameItemMap);
const [gameInventoryItemList, setGameInventoryItemList] = useState<GameInventoryItem[]>(apiSDK.gameInventoryItemList);
const [currentGameShopId, setCurrentGameShopId] = useState<number>();
const { activeModal } = useContext(ModalContext);
const [account, setAccount] = useState(apiSDK.account);
const [currentGame, setCurrentGame] = useState<Game | undefined>(undefined);
const { t } = useTranslation();
Expand Down Expand Up @@ -67,6 +77,13 @@ const Component = ({ className }: Props): React.ReactElement => {
};
}, [exitGame]);

const onOpenShop = useCallback((gameId?: number) => {
return () => {
setCurrentGameShopId(gameId);
activeModal(shopModalId);
};
}, [activeModal]);

useEffect(() => {
const accountSub = apiSDK.subscribeAccount().subscribe((data) => {
setAccount(data);
Expand All @@ -76,9 +93,24 @@ const Component = ({ className }: Props): React.ReactElement => {
setGameList(data);
});

const energyConfigSub = apiSDK.subscribeEnergyConfig().subscribe((data) => {
setEnergyConfig(data);
});

const gameItemMapSub = apiSDK.subscribeGameItemMap().subscribe((data) => {
setGameItemMap(data);
});

const gameInventoryItemListSub = apiSDK.subscribeGameInventoryItemList().subscribe((data) => {
setGameInventoryItemList(data);
});

return () => {
accountSub.unsubscribe();
energyConfigSub.unsubscribe();
gameListSub.unsubscribe();
gameItemMapSub.unsubscribe();
gameInventoryItemListSub.unsubscribe();
};
}, []);

Expand All @@ -92,8 +124,21 @@ const Component = ({ className }: Props): React.ReactElement => {
/>
<GameEnergy
energy={account.attributes.energy}
maxEnergy={energyConfig?.maxEnergy}
startTime={account.attributes.lastEnergyUpdated}
/>

<Button
icon={(
<Icon
phosphorIcon={ShoppingBag}
size='md'
/>
)}
onClick={onOpenShop()}
size='xs'
type='ghost'
/>
</div>}
{gameList.map((game) => (<div
className={CN('game-item', { 'coming-soon': checkComingSoon(game) })}
Expand Down Expand Up @@ -131,13 +176,28 @@ const Component = ({ className }: Props): React.ReactElement => {
</Typography.Title>
</div>
<div className={'play-area'}>
<Button
className={'play-button'}
onClick={playGame(game)}
size={'xs'}
>
{t('Open')}
</Button>
<div>
<Button
icon={(
<Icon
phosphorIcon={ShoppingBag}
size='md'
/>
)}
onClick={onOpenShop(game.id)}
size='xs'
type='ghost'
/>

<Button
className={'play-button'}
onClick={playGame(game)}
size={'xs'}
>
{t('Open')}
</Button>
</div>

<Typography.Text
className={'game-energy'}
size={'sm'}
Expand All @@ -156,6 +216,13 @@ const Component = ({ className }: Props): React.ReactElement => {
src={currentGame.url}
/>
</div>}

<ShopModal
energyConfig={energyConfig}
gameId={currentGameShopId}
gameInventoryItemList={gameInventoryItemList}
gameItemMap={gameItemMap}
/>
</div>;
};

Expand Down
14 changes: 11 additions & 3 deletions packages/extension-koni-ui/src/components/Games/GameEnergy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@ type GameEnergyProps = ThemeProps & {
className?: string;
startTime: string;
energy: number;
maxEnergy?: number;
};

const maxEnergy = 300;
const ONE_SECOND = 1000; // 1 second in milliseconds
const regenSeconds = 60;
const regenTime = ONE_SECOND * regenSeconds;

function _GameEnergy ({ className, energy, startTime }: GameEnergyProps) {
function _GameEnergy ({ className, energy, maxEnergy, startTime }: GameEnergyProps) {
const [countdown, setCountdown] = useState<number | undefined>();
const [currentEnergy, setCurrentEnergy] = useState(energy);
const intervalRef = useRef<NodeJS.Timeout | null>(null);
Expand All @@ -31,6 +31,10 @@ function _GameEnergy ({ className, energy, startTime }: GameEnergyProps) {
}, [startTime]);

const updateEnergy = useCallback(() => {
if (!maxEnergy) {
return;
}

const now = Date.now();
const diff = now - startRegen;
const recovered = Math.floor(diff / regenTime);
Expand All @@ -48,7 +52,7 @@ function _GameEnergy ({ className, energy, startTime }: GameEnergyProps) {
setCurrentEnergy(recovered + energy);
setCountdown(remainingTime);
}
}, [energy, startRegen]);
}, [energy, maxEnergy, startRegen]);

useEffect(() => {
intervalRef.current = setInterval(updateEnergy, ONE_SECOND);
Expand All @@ -60,6 +64,10 @@ function _GameEnergy ({ className, energy, startTime }: GameEnergyProps) {
};
}, [updateEnergy]);

if (!maxEnergy) {
return null;
}

return <div className={className}>
<Progress
percent={currentEnergy / maxEnergy * 100}
Expand Down
170 changes: 170 additions & 0 deletions packages/extension-koni-ui/src/components/Modal/Shop/ShopModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
// Copyright 2019-2022 @subwallet/extension-koni-ui authors & contributors
// SPDX-License-Identifier: Apache-2.0

import { ShopItem } from '@subwallet/extension-koni-ui/components';
import { BookaSdk } from '@subwallet/extension-koni-ui/connector/booka/sdk';
import { EnergyConfig, GameInventoryItem, GameItem } from '@subwallet/extension-koni-ui/connector/booka/types';
import useTranslation from '@subwallet/extension-koni-ui/hooks/common/useTranslation';
import { ThemeProps } from '@subwallet/extension-koni-ui/types';
import { ShopItemInfo } from '@subwallet/extension-koni-ui/types/shop';
import { ModalContext, SwModal } from '@subwallet/react-ui';
import React, { useCallback, useContext, useMemo, useState } from 'react';
import styled from 'styled-components';

type Props = ThemeProps & {
gameId?: number;
energyConfig?: EnergyConfig;
gameItemMap: Record<string, GameItem[]>;
gameInventoryItemList: GameInventoryItem[];
};

export const ShopModalId = 'ShopModalId';
const apiSDK = BookaSdk.instance;

function Component ({ className, energyConfig,
gameId,
gameInventoryItemList, gameItemMap }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const [isLoading, setIsLoading] = useState<boolean>(false);

const { inactiveModal } = useContext(ModalContext);

const onClose = useCallback(() => {
inactiveModal(ShopModalId);
}, [inactiveModal]);

const items = useMemo<ShopItemInfo[]>(() => {
const result: ShopItemInfo[] = [];

if (energyConfig) {
result.push({
gameItemId: 'buy-energy-id',
name: 'Energy',
description: '',
price: energyConfig.energyPrice
});
}

const inventoryItemMapByGameItemId: Record<number, GameInventoryItem> = {};

gameInventoryItemList.forEach((i) => {
inventoryItemMapByGameItemId[i.gameItemId] = i;
});

const getShopItem = (gi: GameItem, disabled = false): ShopItemInfo => {
const limit = gi.maxBuy || undefined;
const inventoryQuantity = inventoryItemMapByGameItemId[gi.id]?.quantity || undefined;

return {
gameItemId: `${gi.id}`,
name: gi.name,
gameId: gi.gameId,
limit,
description: gi.description,
inventoryQuantity,
itemGroup: gi.itemGroup,
itemGroupLevel: gi.itemGroupLevel,
price: gi.price,
disabled: disabled || (!!limit && limit > 0 && limit === inventoryQuantity) || (!!gi.itemGroup && inventoryQuantity === 1),
usable: !!inventoryQuantity && inventoryQuantity > 0 && inventoryItemMapByGameItemId[gi.id]?.usable
};
};

[...Object.keys(gameItemMap)].forEach((groupKey) => {
if (groupKey !== 'NO_GROUP' && gameItemMap[groupKey][0]?.effectDuration === -1) {
const noQuantityItems = gameItemMap[groupKey].filter((gi) => !inventoryItemMapByGameItemId[gi.id]?.quantity);

let itemPresentForGroup: GameItem;

if (noQuantityItems.length) {
itemPresentForGroup = noQuantityItems.reduce((item, currentItem) => {
return currentItem.itemGroupLevel && item.itemGroupLevel && currentItem.itemGroupLevel < item.itemGroupLevel ? currentItem : item;
}, { itemGroupLevel: Number.POSITIVE_INFINITY } as GameItem);

if (itemPresentForGroup.itemGroupLevel !== Number.POSITIVE_INFINITY) {
result.push(getShopItem(itemPresentForGroup));
}
} else {
itemPresentForGroup = gameItemMap[groupKey]
.reduce((item, currentItem) => {
return currentItem.itemGroupLevel && item.itemGroupLevel && currentItem.itemGroupLevel > item.itemGroupLevel ? currentItem : item;
}, { itemGroupLevel: Number.NEGATIVE_INFINITY } as GameItem);

if (itemPresentForGroup.itemGroupLevel !== Number.NEGATIVE_INFINITY) {
result.push(getShopItem(itemPresentForGroup, true));
}
}

return;
}

gameItemMap[groupKey].forEach((gi) => {
if ((!gameId && !gi.gameId) || (gameId && gi.gameId === gameId)) {
result.push(getShopItem(gi));
}
});
});

return result;
}, [energyConfig, gameId, gameInventoryItemList, gameItemMap]);

const onBuy = useCallback((gameItemId: string) => {
setIsLoading(true);

if (gameItemId === 'buy-energy-id') {
apiSDK.buyEnergy().catch((e) => {
console.log('buyEnergy error', e);
}).finally(() => {
setIsLoading(false);
});
} else {
apiSDK.buyItem(+gameItemId).catch((e) => {
console.log('buyItem error', e);
}).finally(() => {
setIsLoading(false);
});
}
}, []);

const onUse = useCallback((gameItemId: string) => {
setIsLoading(true);

apiSDK.useInventoryItem(+gameItemId).catch((e) => {
console.log('onUse error', e);
}).finally(() => {
setIsLoading(false);
});
}, []);

return (
<SwModal
className={className}
id={ShopModalId}
onCancel={onClose}
title={t('Items')}
>
{
items.map((item) => (
<ShopItem
className={'shop-item'}
key={item.gameItemId}
{...item}
disabled={isLoading || item.disabled}
onBuy={onBuy}
onUse={onUse}
/>
))
}
</SwModal>
);
}

const ShopModal = styled(Component)<Props>(({ theme: { token } }: Props) => {
return ({
'.shop-item + .shop-item': {
marginTop: token.marginSM
}
});
});

export default ShopModal;
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Copyright 2019-2022 @subwallet/extension-koni-ui authors & contributors
// SPDX-License-Identifier: Apache-2.0

export { default as ShopModal } from './ShopModal';
1 change: 1 addition & 0 deletions packages/extension-koni-ui/src/components/Modal/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ export * from './GlobalSearchTokenModal';
export * from './ReceiveModal';
export * from './Common';
export * from './Announcement';
export * from './Shop';
Loading

0 comments on commit de4d9c8

Please sign in to comment.