diff --git a/App.tsx b/App.tsx index 4968d8f..fc9c42c 100644 --- a/App.tsx +++ b/App.tsx @@ -2,10 +2,28 @@ import MultiplayerProvider from '@contexts/MultiplayerContext' import { config } from '@gluestack-ui/config' import { GluestackUIProvider } from '@gluestack-ui/themed' import WebLayout from '@layouts/WebLayout' +import { VersionDatabaseService } from '@services/database/version.database' +import { useEffect, useState } from 'react' import { Platform } from 'react-native' import 'react-native-gesture-handler' export default function App() { + // #region States + const [isValidatingVersion, setIsValidatingVersion] = useState(true) + // #endregion + + // #region Effects + useEffect(() => { + const versionPromise = VersionDatabaseService.validate() + + Promise.allSettled([versionPromise]).then(() => { + setIsValidatingVersion(false) + }) + }, []) + // #endregion + + if (isValidatingVersion) return <> + return ( {Platform.OS === 'web' ? : <>} diff --git a/assets/dogs/16.png b/assets/dogs/16.png index aca6fcf..26467d1 100644 Binary files a/assets/dogs/16.png and b/assets/dogs/16.png differ diff --git a/assets/dogs/2.png b/assets/dogs/2.png index e73afe9..3c1faae 100644 Binary files a/assets/dogs/2.png and b/assets/dogs/2.png differ diff --git a/assets/dogs/256.png b/assets/dogs/256.png index d5ea9c3..21a60bf 100644 Binary files a/assets/dogs/256.png and b/assets/dogs/256.png differ diff --git a/assets/dogs/32.png b/assets/dogs/32.png index aac8569..c028bb6 100644 Binary files a/assets/dogs/32.png and b/assets/dogs/32.png differ diff --git a/assets/dogs/4.png b/assets/dogs/4.png index cbcd101..8cc5a76 100644 Binary files a/assets/dogs/4.png and b/assets/dogs/4.png differ diff --git a/assets/dogs/64.jpg b/assets/dogs/64.jpg index b808775..24cfd2f 100644 Binary files a/assets/dogs/64.jpg and b/assets/dogs/64.jpg differ diff --git a/assets/dogs/8.png b/assets/dogs/8.png index 2f72f56..d357301 100644 Binary files a/assets/dogs/8.png and b/assets/dogs/8.png differ diff --git a/package-lock.json b/package-lock.json index fe5d19b..4945df4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "2040dog", - "version": "1.0.0", + "version": "2.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "2040dog", - "version": "1.0.0", + "version": "2.0.0", "dependencies": { "@expo/metro-runtime": "~3.1.3", "@expo/vector-icons": "^14.0.0", @@ -18,6 +18,7 @@ "expo": "~50.0.14", "expo-clipboard": "~5.0.1", "expo-status-bar": "~1.11.1", + "lodash": "^4.17.21", "lucide-react-native": "^0.377.0", "peerjs": "^1.5.2", "react": "18.2.0", @@ -32,6 +33,7 @@ }, "devDependencies": { "@babel/core": "^7.20.0", + "@types/lodash": "^4.17.4", "@types/react": "~18.2.45", "@types/uuid": "^9.0.8", "eslint": "^8.57.0", @@ -8290,6 +8292,13 @@ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "dev": true }, + "node_modules/@types/lodash": { + "version": "4.17.4", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.4.tgz", + "integrity": "sha512-wYCP26ZLxaT3R39kiN2+HcJ4kTd3U1waI/cY7ivWYqFP6pW3ZNpvi6Wd6PHZx7T/t8z0vlkXMg3QYLa7DZ/IJQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "20.12.7", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.7.tgz", @@ -14240,7 +14249,8 @@ "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" }, "node_modules/lodash.debounce": { "version": "4.0.8", diff --git a/package.json b/package.json index 1bd6068..637f3a0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "2040dog", - "version": "1.0.0", + "version": "2.0.0", "main": "node_modules/expo/AppEntry.js", "scripts": { "export:web": "expo export --platform web", @@ -20,7 +20,9 @@ "@gluestack-ui/themed": "^1.1.22", "@react-native-async-storage/async-storage": "1.21.0", "expo": "~50.0.14", + "expo-clipboard": "~5.0.1", "expo-status-bar": "~1.11.1", + "lodash": "^4.17.21", "lucide-react-native": "^0.377.0", "peerjs": "^1.5.2", "react": "18.2.0", @@ -31,11 +33,11 @@ "react-native-reanimated": "~3.6.2", "react-native-svg": "^14.1.0", "react-native-web": "~0.19.6", - "uuid": "^9.0.1", - "expo-clipboard": "~5.0.1" + "uuid": "^9.0.1" }, "devDependencies": { "@babel/core": "^7.20.0", + "@types/lodash": "^4.17.4", "@types/react": "~18.2.45", "@types/uuid": "^9.0.8", "eslint": "^8.57.0", diff --git a/src/contexts/BoardContext.tsx b/src/contexts/BoardContext.tsx index 895639b..b68d962 100644 --- a/src/contexts/BoardContext.tsx +++ b/src/contexts/BoardContext.tsx @@ -2,9 +2,9 @@ import { IBoard } from '@interfaces/board' import { IDirection } from '@interfaces/direction' import { IMove } from '@interfaces/move' import { IPosition } from '@interfaces/position' -import { ITile } from '@interfaces/tile' import BoardDatabaseService from '@services/database/board.database' import React, { createContext, useContext, useReducer } from 'react' +import { v4 as uuidv4 } from 'uuid' // #region Context types interface IBoardProviderProps { @@ -36,6 +36,9 @@ type IBoardContextAction = | { type: 'restart' } + | { + type: 'clean-up' + } // #endregion // #region Constant variables @@ -44,50 +47,49 @@ const boardLength = 4 // #region Functions function createNewBoard() { - const tiles: ITile[][] = [] + const tiles: IBoard['tiles'] = [] for (let i = 0; i < boardLength; i++) { tiles.push([]) for (let j = 0; j < boardLength; j++) { - tiles[i].push({ value: null, isCombined: false, isNew: false }) + tiles[i].push(null) } } return { tiles } as IBoard } -function createTestBoard() { +function createTestBoard(): IBoard { return { tiles: [ [ - { value: 2, isCombined: false }, - { value: 4, isCombined: false }, - { value: 8, isCombined: false }, - { value: 16, isCombined: false }, - ], - [ - { value: 32, isCombined: false }, - { value: 64, isCombined: false }, - { value: 128, isCombined: false }, - { value: 256, isCombined: false }, + { value: 2, isCombined: false, isNew: false, ids: [uuidv4()] }, + { value: 4, isCombined: false, isNew: false, ids: [uuidv4()] }, + { value: 8, isCombined: false, isNew: false, ids: [uuidv4()] }, + { value: 16, isCombined: false, isNew: false, ids: [uuidv4()] }, ], [ - { value: 512, isCombined: false }, - { value: 1024, isCombined: false }, - { value: 2048, isCombined: false }, - { value: null, isCombined: false }, + { value: 32, isCombined: false, isNew: false, ids: [uuidv4()] }, + { value: 64, isCombined: false, isNew: false, ids: [uuidv4()] }, + { value: 128, isCombined: false, isNew: false, ids: [uuidv4()] }, + { value: 256, isCombined: false, isNew: false, ids: [uuidv4()] }, ], [ - { value: null, isCombined: false }, - { value: null, isCombined: false }, - { value: null, isCombined: false }, - { value: null, isCombined: false }, + { value: 512, isCombined: false, isNew: false, ids: [uuidv4()] }, + { value: 1024, isCombined: false, isNew: false, ids: [uuidv4()] }, + { value: 2048, isCombined: false, isNew: false, ids: [uuidv4()] }, + null, ], + [null, null, null, null], ], - } as IBoard + } } function deepCopyBoard(board: IBoard): IBoard { return { - tiles: [...board.tiles.map((row) => [...row.map((tile) => ({ ...tile }))])], + tiles: [ + ...board.tiles.map((row) => [ + ...row.map((tile) => (tile === null ? null : { ...tile })), + ]), + ], } } @@ -97,7 +99,7 @@ function listEmptySpots(board: IBoard) { tiles.forEach((row, i) => row.forEach((tile, j) => { - if (tile.value === null) { + if (tile === null) { emptySpots.push({ i, j }) } }), @@ -117,8 +119,12 @@ function insertRandomTile(board: IBoard) { const randomSpotIndex = Math.floor(Math.random() * emptySpots.length) const randomSpot = emptySpots[randomSpotIndex] - board.tiles[randomSpot.i][randomSpot.j].value = newRandomTileValue() - board.tiles[randomSpot.i][randomSpot.j].isNew = true + board.tiles[randomSpot.i][randomSpot.j] = { + value: newRandomTileValue(), + isNew: true, + isCombined: false, + ids: [uuidv4()], + } return board } @@ -128,7 +134,7 @@ function getTile(board: IBoard, position: IPosition) { } function isBoardFull(board: IBoard) { - return board.tiles.every((row) => row.every((tile) => tile.value !== null)) + return board.tiles.every((row) => row.every((tile) => tile !== null)) } function isPossibleToMove(board: IBoard) { @@ -137,10 +143,10 @@ function isPossibleToMove(board: IBoard) { board.tiles.some((row, i, tiles) => row.some((tile, j) => { return ( - (i > 0 && tiles[i - 1][j].value === tile.value) || - (i < boardLength - 1 && tiles[i + 1][j].value === tile.value) || - (j > 0 && tiles[i][j - 1].value === tile.value) || - (j < boardLength - 1 && tiles[i][j + 1].value === tile.value) + (i > 0 && tiles[i - 1][j]!.value === tile!.value) || + (i < boardLength - 1 && tiles[i + 1][j]!.value === tile!.value) || + (j > 0 && tiles[i][j - 1]!.value === tile!.value) || + (j < boardLength - 1 && tiles[i][j + 1]!.value === tile!.value) ) }), ) @@ -148,14 +154,17 @@ function isPossibleToMove(board: IBoard) { } function doesBoardHave2048(board: IBoard) { - return board.tiles.some((row) => row.some((tile) => tile.value === 2048)) + return board.tiles.some((row) => row.some((tile) => tile?.value === 2048)) } function resetCombinedAndNewStates(board: IBoard) { board.tiles.forEach((row) => { - row.forEach((_, idx, arr) => { - arr[idx].isCombined = false - arr[idx].isNew = false + row.forEach((item) => { + if (item !== null) { + item.isCombined = false + item.isNew = false + item.ids = [item.ids[0]] + } }) }) @@ -169,29 +178,30 @@ function move(board: IBoard, direction: IDirection): IMove[] { case 'up': { board.tiles.forEach((row, i, tiles) => row.forEach((tile, j) => { - if (i === 0 || tile.value === null) return + if (i === 0 || tile === null) return let nextI = i - while (nextI > 0 && tiles[nextI - 1][j].value === null) { + while (nextI > 0 && tiles[nextI - 1][j] === null) { nextI-- } if ( nextI > 0 && - tiles[nextI - 1][j].value === tile.value && - !tiles[nextI - 1][j].isCombined + tiles[nextI - 1][j]!.value === tile.value && + !tiles[nextI - 1][j]!.isCombined ) { tiles[nextI - 1][j] = { value: tile.value * 2, isCombined: true, isNew: false, + ids: [tiles[nextI - 1][j]!.ids[0], tile.ids[0]], } } else { if (nextI === i) return tiles[nextI][j] = { ...tile } } - tiles[i][j] = { value: null, isCombined: false, isNew: false } + tiles[i][j] = null moves.push({ previous: { i, j }, @@ -212,32 +222,30 @@ function move(board: IBoard, direction: IDirection): IMove[] { const row = board.tiles[i] return row.forEach((tile, j) => { - if (i === boardLength - 1 || tile.value === null) return + if (i === boardLength - 1 || tile === null) return let nextI = i - while ( - nextI < boardLength - 1 && - tiles[nextI + 1][j].value === null - ) { + while (nextI < boardLength - 1 && tiles[nextI + 1][j] === null) { nextI++ } if ( nextI < boardLength - 1 && - tiles[nextI + 1][j].value === tile.value && - !tiles[nextI + 1][j].isCombined + tiles[nextI + 1][j]!.value === tile.value && + !tiles[nextI + 1][j]!.isCombined ) { tiles[nextI + 1][j] = { value: tile.value * 2, isCombined: true, isNew: false, + ids: [tiles[nextI + 1][j]!.ids[0], tile.ids[0]], } } else { if (nextI === i) return tiles[nextI][j] = { ...tile } } - tiles[i][j] = { value: null, isCombined: false, isNew: false } + tiles[i][j] = null moves.push({ previous: { i, j }, @@ -251,29 +259,30 @@ function move(board: IBoard, direction: IDirection): IMove[] { case 'left': { board.tiles.forEach((row, i, tiles) => row.forEach((tile, j) => { - if (j === 0 || tile.value === null) return + if (j === 0 || tile === null) return let nextJ = j - while (nextJ > 0 && tiles[i][nextJ - 1].value === null) { + while (nextJ > 0 && tiles[i][nextJ - 1] === null) { nextJ-- } if ( nextJ > 0 && - tiles[i][nextJ - 1].value === tile.value && - !tiles[i][nextJ - 1].isCombined + tiles[i][nextJ - 1]!.value === tile.value && + !tiles[i][nextJ - 1]!.isCombined ) { tiles[i][nextJ - 1] = { value: tile.value * 2, isCombined: true, isNew: false, + ids: [tiles[i][nextJ - 1]!.ids[0], tile.ids[0]], } } else { if (nextJ === j) return tiles[i][nextJ] = { ...tile } } - tiles[i][j] = { value: null, isCombined: false, isNew: false } + tiles[i][j] = null moves.push({ previous: { i, j }, @@ -292,29 +301,30 @@ function move(board: IBoard, direction: IDirection): IMove[] { return reversedIndexes.forEach((j) => { const tile = tiles[i][j] - if (j === boardLength - 1 || tile.value === null) return + if (j === boardLength - 1 || tile === null) return let nextJ = j - while (nextJ < boardLength - 1 && tiles[i][nextJ + 1].value == null) { + while (nextJ < boardLength - 1 && tiles[i][nextJ + 1] == null) { nextJ++ } if ( nextJ < boardLength - 1 && - tiles[i][nextJ + 1].value === tile.value && - !tiles[i][nextJ + 1].isCombined + tiles[i][nextJ + 1]!.value === tile.value && + !tiles[i][nextJ + 1]!.isCombined ) { tiles[i][nextJ + 1] = { value: tile.value * 2, isCombined: true, isNew: false, + ids: [tiles[i][nextJ + 1]!.ids[0], tile.ids[0]], } } else { if (nextJ === j) return tiles[i][nextJ] = { ...tile } } - tiles[i][j] = { value: null, isCombined: false, isNew: false } + tiles[i][j] = null moves.push({ previous: { i, j }, @@ -333,8 +343,7 @@ function move(board: IBoard, direction: IDirection): IMove[] { function findGreatestTileValue(board: IBoard): number { return board.tiles.reduce((prevGreatest, currRow) => { const greatestInRow = currRow.reduce( - (prev, curr) => - curr.value !== null && curr.value > prev ? curr.value : prev, + (prev, curr) => (curr !== null && curr.value > prev ? curr.value : prev), 0, ) @@ -348,7 +357,7 @@ function countPointsOfCombined(board: IBoard): number { prevTotalSum + currRow.reduce( (prev, curr) => - prev + (curr.isCombined && curr.value !== null ? curr.value : 0), + prev + (curr !== null && curr.isCombined ? curr.value : 0), 0, ), 0, @@ -467,6 +476,7 @@ function BoardReducer( } } default: { + // includes 'clean-up' return state } } diff --git a/src/hooks/use-prev-props.ts b/src/hooks/use-prev-props.ts new file mode 100644 index 0000000..d524662 --- /dev/null +++ b/src/hooks/use-prev-props.ts @@ -0,0 +1,11 @@ +import { useEffect, useRef } from 'react' + +export default function usePrevProps(value: K) { + const ref = useRef() + + useEffect(() => { + ref.current = value + }) + + return ref.current +} diff --git a/src/interfaces/board.ts b/src/interfaces/board.ts index 8301aab..fcd0370 100644 --- a/src/interfaces/board.ts +++ b/src/interfaces/board.ts @@ -1,5 +1,5 @@ import { ITile } from './tile' export interface IBoard { - tiles: ITile[][] + tiles: (ITile | null)[][] } diff --git a/src/interfaces/tile.ts b/src/interfaces/tile.ts index ad3aa36..8f2f144 100644 --- a/src/interfaces/tile.ts +++ b/src/interfaces/tile.ts @@ -1,5 +1,6 @@ export interface ITile { - value: number | null + ids: string[] + value: number isCombined: boolean isNew: boolean } diff --git a/src/services/database/version.database.ts b/src/services/database/version.database.ts new file mode 100644 index 0000000..bb0f0d9 --- /dev/null +++ b/src/services/database/version.database.ts @@ -0,0 +1,19 @@ +import AsyncStorage from "@react-native-async-storage/async-storage" +import { version } from "@utils/constants/version" +import BoardDatabaseService from "./board.database" + +export class VersionDatabaseService { + private static versionKey = 'version' + + static async validate() { + try { + const currVersion = await AsyncStorage.getItem(VersionDatabaseService.versionKey) + if (currVersion !== version) { + await BoardDatabaseService.delete() + await AsyncStorage.setItem(VersionDatabaseService.versionKey, version) + } + } catch (e) { + if (e instanceof Error) console.debug(e.message) + } + } +} \ No newline at end of file diff --git a/src/utils/constants/animation.ts b/src/utils/constants/animation.ts new file mode 100644 index 0000000..7d686cf --- /dev/null +++ b/src/utils/constants/animation.ts @@ -0,0 +1,3 @@ +export const isNewAnimationDuration = 150 +export const combinedAnimationDuration = 100 +export const moveAnimationDuration = 100 diff --git a/src/utils/constants/version.ts b/src/utils/constants/version.ts new file mode 100644 index 0000000..708a671 --- /dev/null +++ b/src/utils/constants/version.ts @@ -0,0 +1 @@ +export const version = '2.0.0' \ No newline at end of file diff --git a/src/utils/helpers/tile-color-by-value.ts b/src/utils/helpers/tile-color-by-value.ts index 101a1d6..cdb8f46 100644 --- a/src/utils/helpers/tile-color-by-value.ts +++ b/src/utils/helpers/tile-color-by-value.ts @@ -9,7 +9,7 @@ interface ITileStyle { } } -const styleMap: Record = { +export const styleMap: Record = { 2: { bgColor: '$coolGray200', textColor: '$black', diff --git a/src/views/Game/Board.tsx b/src/views/Game/Board.tsx index 25b1110..c08b7ae 100644 --- a/src/views/Game/Board.tsx +++ b/src/views/Game/Board.tsx @@ -1,7 +1,8 @@ -import { HStack, VStack } from '@gluestack-ui/themed' +import { Box, View } from '@gluestack-ui/themed' import { IBoard } from '@interfaces/board' import Tile from './Tile' +import BackgroundTile from './components/BackgroundTile' interface IBoardProps { board: IBoard @@ -17,28 +18,50 @@ export default function Board(props: Readonly) { // #endregion return ( - - {board.tiles.map((row, rowIdx) => ( - - {row.map((tile, columnIdx) => ( - - ))} - + + {['bg', 'game'].map((id) => ( + + {board.tiles + .map((row, rowIdx) => + row.map((tile, columnIdx) => { + if (id === 'bg') { + return ( + + ) + } + + if (tile === null) return false + + return ( + + ) + }), + ) + .flat()} + ))} - + ) } diff --git a/src/views/Game/MainPage.tsx b/src/views/Game/MainPage.tsx index 4935a6c..3c28b5c 100644 --- a/src/views/Game/MainPage.tsx +++ b/src/views/Game/MainPage.tsx @@ -36,7 +36,9 @@ import { IBoard } from '@interfaces/board' import { IDirection } from '@interfaces/direction' import BoardDatabaseService from '@services/database/board.database' import PointsDatabaseService from '@services/database/points.database' +import { moveAnimationDuration } from '@utils/constants/animation' import * as Clipboard from 'expo-clipboard' +import { throttle } from 'lodash' import { Check } from 'lucide-react-native' import { DataConnection } from 'peerjs' import { useCallback, useEffect, useState } from 'react' @@ -102,7 +104,7 @@ export default function MainPage() { .shouldCancelWhenOutside(true) .runOnJS(true) .onEnd(() => { - boardDispatch({ type: 'move', direction: direction as IDirection }) + move(direction as IDirection) }), ) const moveGesture = Gesture.Race(...gestures) @@ -210,6 +212,18 @@ export default function MainPage() { ], ) + // eslint-disable-next-line react-hooks/exhaustive-deps + const move = useCallback( + throttle( + (direction: IDirection) => { + boardDispatch({ type: 'move', direction }) + }, + moveAnimationDuration * 1.05, + { trailing: false }, + ), + [boardDispatch], + ) + function multiplayerConnectToPeer() { if (typeRemotePlayerId === currPlayerId) { setIsConnectingToPlayer(false) @@ -257,12 +271,13 @@ export default function MainPage() { if (direction === undefined) return e.preventDefault() - boardDispatch({ type: 'move', direction }) + + move(direction) } window.addEventListener('keydown', keyDownHandler) return () => window.removeEventListener('keydown', keyDownHandler) - }, [boardDispatch]) + }, [move]) // onAfterMove useEffect(() => { @@ -274,7 +289,9 @@ export default function MainPage() { } if (isGameOver) return - boardDispatch({ type: 'insert' }) + setTimeout(() => { + boardDispatch({ type: 'insert' }) + }, moveAnimationDuration) if (isMultiplayer && peerConnection) { peerConnection.send({ board, isGameOver, hasWon }) @@ -554,9 +571,12 @@ export default function MainPage() { )} diff --git a/src/views/Game/Tile.tsx b/src/views/Game/Tile.tsx index 6a59de2..64ace3f 100644 --- a/src/views/Game/Tile.tsx +++ b/src/views/Game/Tile.tsx @@ -1,27 +1,33 @@ import { AnimatedView } from '@gluestack-style/animation-resolver' -import { Box, Text, styled } from '@gluestack-ui/themed' +import { Text, styled } from '@gluestack-ui/themed' import tileColorByValue from '@helpers/tile-color-by-value' -import { memo, useEffect, useMemo, useState } from 'react' +import usePrevProps from '@hooks/use-prev-props' +import { IPosition } from '@interfaces/position' +import { ITile } from '@interfaces/tile' +import { + combinedAnimationDuration, + isNewAnimationDuration, + moveAnimationDuration, +} from '@utils/constants/animation' +import { memo, useCallback, useEffect, useMemo, useState } from 'react' import TileImage from './TileImage' interface ITileProps { i: number j: number - value?: number | null - hasBeenCombined: boolean - isNew: boolean + tile: ITile } const Tile = (props: Readonly) => { // #region Props - const { value, hasBeenCombined, isNew, i, j } = props + const { tile, i, j } = props + const { isCombined: hasBeenCombined, isNew, value } = tile // #endregion // #region Constant variables - const isNewAnimationDuration = 150 - const combinedAnimationDuration = 100 const bgColorNull = '$trueGray300' + const tileTotalLength = (1 + 16 + 1) * 4 // #endregion // #region States @@ -29,7 +35,7 @@ const Tile = (props: Readonly) => { const [combinedAnimationState, setCombinedAnimationState] = useState(0) // #endregion - // #region Hooks + // #region Memos const { bgColor, textColor, image } = useMemo( () => value @@ -51,6 +57,17 @@ const Tile = (props: Readonly) => { }, [isNewAnimationState, combinedAnimationState]) // #endregion + // #region Prev props + const previousPosition = usePrevProps({ i, j }) + // #endregion + + // #region Callbacks + const positionToPixels = useCallback( + (position: number) => position * tileTotalLength, + [tileTotalLength], + ) + // #endregion + // #region Effects // onNew @@ -71,7 +88,9 @@ const Tile = (props: Readonly) => { // onCombined useEffect(() => { - if (hasBeenCombined) setCombinedAnimationState(1) + if (hasBeenCombined) { + setTimeout(() => setCombinedAnimationState(1), moveAnimationDuration) + } }, [hasBeenCombined]) // onCombinedAnimation @@ -95,14 +114,31 @@ const Tile = (props: Readonly) => { // #region Styled components const AnimatedBox = styled(AnimatedView, { - minHeight: '$16', - minWidth: '$16', - maxHeight: '$80', - maxWidth: '$80', + position: 'absolute', + h: '$16', + w: '$16', + margin: '$1', backgroundColor: bgColorNull, - ':initial': { scale: startScale }, - ':animate': { scale: stopScale }, + borderRadius: '$md', + ':initial': { + x: positionToPixels(previousPosition?.j ?? j), + y: positionToPixels(previousPosition?.i ?? i), + scale: startScale, + }, + ':animate': { + x: positionToPixels(j), + y: positionToPixels(i), + scale: stopScale, + }, ':transition': { + x: { + duration: moveAnimationDuration, + ease: 'easeIn', + }, + y: { + duration: moveAnimationDuration, + ease: 'easeIn', + }, scale: { duration: isNewAnimationState === 1 @@ -117,49 +153,48 @@ const Tile = (props: Readonly) => { if (isNew && isNewAnimationState === 0) { return ( - - - - - - ) - } - - return ( - - {image && } - = 1024 ? '$xl' : '$3xl'} - fontWeight="$bold" - > - {value} - + - + ) + } + + return ( + + + = 1024 ? '$xl' : '$3xl'} + fontWeight="$bold" + > + {value} + + ) } export default memo(Tile, (prev, next) => { - if (prev.isNew && !next.isNew && prev.value === next.value) return true + if ( + prev.tile.isNew && + !next.tile.isNew && + prev.tile.value === next.tile.value + ) { + return true + } return ( - prev.hasBeenCombined === next.hasBeenCombined && - prev.value === next.value && - prev.isNew === next.isNew && + prev.tile.isCombined === next.tile.isCombined && + prev.tile.value === next.tile.value && + prev.tile.isNew === next.tile.isNew && prev.i === next.i && prev.j === next.j ) diff --git a/src/views/Game/TileImage.tsx b/src/views/Game/TileImage.tsx index 4d5d12d..b9ab180 100644 --- a/src/views/Game/TileImage.tsx +++ b/src/views/Game/TileImage.tsx @@ -1,20 +1,29 @@ -import { Image } from '@gluestack-ui/themed' import { memo } from 'react' +import { Image } from 'react-native' interface ITileImageProps { - source: string - alt: string + image?: { + source: any + alt: string + } } const TileImage = (props: Readonly) => { + // #region Props + const { image } = props + // #endregion + return ( {props.alt} ) } diff --git a/src/views/Game/components/BackgroundTile.tsx b/src/views/Game/components/BackgroundTile.tsx new file mode 100644 index 0000000..1320ea2 --- /dev/null +++ b/src/views/Game/components/BackgroundTile.tsx @@ -0,0 +1,18 @@ +import { Box, Text } from '@gluestack-ui/themed' +import { memo } from 'react' + +const BackgroundTile = () => { + return ( + + + + ) +} + +export default memo(BackgroundTile) diff --git a/tsconfig.json b/tsconfig.json index 06e6bf5..0194751 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,7 +9,9 @@ "@services/*": ["./src/services/*"], "@views/*": ["./src/views/*"], "@layouts/*": ["./src/layouts/*"], - "@assets/*": ["./assets/*"] + "@assets/*": ["./assets/*"], + "@utils/*": ["./src/utils/*"], + "@hooks/*": ["./src/hooks/*"], } }, "include": ["**/*.ts", "**/*.tsx"]