Skip to content
GitLab
Projects
Groups
Snippets
/
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in
Toggle navigation
Menu
Open sidebar
Ondřej Bazala
PV247
Commits
410071e2
Unverified
Commit
410071e2
authored
Nov 21, 2021
by
Michal Čaniga
Browse files
Finish game mechanics, polish the website
parent
1fee183d
Changes
26
Hide whitespace changes
Inline
Side-by-side
public/ash.png
0 → 100644
View file @
410071e2
25.9 KB
src/App.tsx
View file @
410071e2
...
...
@@ -32,6 +32,8 @@ import {
import
{
GameInputProvider
}
from
'
./hooks/useGameInput
'
;
import
{
AllUserDataProvider
}
from
'
./hooks/useAllUserData
'
;
import
GamePage
from
'
./pages/Game/GamePage
'
;
import
{
NotFoundPage
}
from
'
./pages/NotFoundPage
'
;
import
{
Forbidden
}
from
'
./pages/Forbidden
'
;
const
TopMenu
=
()
=>
{
const
{
user
}
=
useLoggedInUser
();
...
...
@@ -98,14 +100,23 @@ const AppContent = () => {
<
Content
style
=
{
{
padding
:
'
0 50px
'
,
height
:
'
95%
'
}
}
>
<
Switch
>
<
Route
path
=
"/"
exact
component
=
{
Home
}
/>
{
isLoggedIn
&&
(
<>
<
Route
path
=
"/store"
exact
component
=
{
Store
}
/>
<
Route
path
=
"/encyclopedia"
exact
component
=
{
Encyclopedia
}
/>
<
Route
path
=
"/leaderboard"
exact
component
=
{
Leaderboard
}
/>
<
Route
path
=
"/game"
exact
component
=
{
GamePage
}
/>
</>
)
}
<
Route
path
=
"/store"
exact
component
=
{
isLoggedIn
?
Store
:
Forbidden
}
/>
<
Route
path
=
"/encyclopedia"
exact
component
=
{
isLoggedIn
?
Encyclopedia
:
Forbidden
}
/>
<
Route
path
=
"/leaderboard"
exact
component
=
{
isLoggedIn
?
Leaderboard
:
Forbidden
}
/>
<
Route
path
=
"/game"
exact
component
=
{
isLoggedIn
?
GamePage
:
Forbidden
}
/>
<
Route
path
=
"*"
component
=
{
NotFoundPage
}
/>
</
Switch
>
</
Content
>
);
...
...
src/data/constants.ts
View file @
410071e2
export
const
maxHearts
=
5
;
export
const
catchReward
=
10
;
export
const
pokemonSampleCount
=
2
;
export
const
esh
=
{
x
:
0
,
...
...
src/data/food.ts
View file @
410071e2
...
...
@@ -20,6 +20,12 @@ export const food: Food[] = [
name
:
'
Chicken Wings
'
,
price
:
471
,
restores
:
2
},
{
id
:
3
,
name
:
'
Chicken Nuggets
'
,
price
:
682
,
restores
:
3
}
];
...
...
src/hooks/useAllUserData.tsx
View file @
410071e2
...
...
@@ -10,7 +10,7 @@ export const AllUserDataProvider: FC = ({ children }) => {
useEffect
(()
=>
{
const
unsubscribe
=
onSnapshot
(
userDataCollection
,
snapshot
=>
{
setAllUserData
(
snapshot
.
docs
.
map
(
doc
=>
doc
.
data
()));
setAllUserData
(
snapshot
.
docs
.
map
(
doc
=>
({
...
doc
.
data
()
,
id
:
doc
.
id
})
));
});
return
()
=>
{
unsubscribe
();
...
...
src/hooks/useLoggedInUser.tsx
View file @
410071e2
...
...
@@ -24,7 +24,10 @@ export const UserProvider: FC = ({ children }) => {
if
(
user
===
undefined
)
{
return
;
}
const
allUserData
=
snapshot
.
docs
.
map
(
doc
=>
doc
.
data
());
const
allUserData
=
snapshot
.
docs
.
map
(
doc
=>
({
...
doc
.
data
(),
id
:
doc
.
id
}));
setUserData
(
allUserData
.
filter
(
ud
=>
ud
.
userId
===
user
.
uid
)[
0
]);
});
return
()
=>
{
...
...
src/pages/Forbidden.tsx
0 → 100644
View file @
410071e2
import
{
FrownOutlined
}
from
'
@ant-design/icons
'
;
import
{
Result
}
from
'
antd
'
;
import
{
useState
}
from
'
react
'
;
import
{
AuthModal
,
AuthModalType
}
from
'
./Home/AuthModal
'
;
import
{
SignInButton
,
SignUpButton
}
from
'
./Home/Home
'
;
export
const
Forbidden
=
()
=>
{
const
[
isAuthModalVisible
,
setIsAuthModalVisible
]
=
useState
(
false
);
const
[
authModalType
,
setAuthModalType
]
=
useState
<
AuthModalType
>
();
return
(
<
Result
status
=
"403"
title
=
"400"
subTitle
=
{
<
span
>
<
FrownOutlined
style
=
{
{
marginRight
:
3
}
}
/>
Sorry, we don`t know who you are yet
<
FrownOutlined
style
=
{
{
marginLeft
:
3
}
}
/>
</
span
>
}
extra
=
{
<>
<
SignInButton
setAuthModalType
=
{
setAuthModalType
}
setIsAuthModalVisible
=
{
setIsAuthModalVisible
}
/>
<
SignUpButton
setAuthModalType
=
{
setAuthModalType
}
setIsAuthModalVisible
=
{
setIsAuthModalVisible
}
/>
<
AuthModal
type
=
{
authModalType
}
isVisible
=
{
isAuthModalVisible
}
setAuthModalType
=
{
setAuthModalType
}
setIsAuthModalVisible
=
{
setIsAuthModalVisible
}
/>
</>
}
/>
);
};
src/pages/Game/BottomBar.tsx
View file @
410071e2
import
{
TrademarkCircleOutlined
}
from
'
@ant-design/icons
'
;
import
{
Row
,
Col
}
from
'
antd
'
;
import
{
useMemo
}
from
'
react
'
;
import
{
getUserFood
}
from
'
../../data/food
'
;
import
{
getUserPokeballs
}
from
'
../../data/pokeballs
'
;
import
{
useLoggedInUser
}
from
'
../../hooks/useLoggedInUser
'
;
import
{
SetPokemonsAlive
,
SetScore
,
SetUserData
}
from
'
../../utils/commonTypes
'
;
import
{
UserData
}
from
'
../../utils/firebase
'
;
import
{
Loading
}
from
'
../../utils/Loading
'
;
import
{
PokemonOnCanvas
}
from
'
../../utils/pokemonFetcher
'
;
import
{
FoodPowerup
}
from
'
./FoodPowerup
'
;
import
{
Powerup
}
from
'
./Powerup
'
;
import
{
Pokeball
Powerup
}
from
'
./
Pokeball
Powerup
'
;
type
BottomBarProps
=
{
style
?:
React
.
CSSProperties
;
setHearts
:
(
hearts
:
number
)
=>
void
;
currentHearts
:
number
;
newUserData
:
UserData
;
setScore
:
SetScore
;
setNewUserData
:
SetUserData
;
alivePokemons
:
PokemonOnCanvas
[];
setAlivePokemons
:
SetPokemonsAlive
;
};
export
const
BottomBar
=
({
style
,
setHearts
,
currentHearts
currentHearts
,
newUserData
,
setScore
,
setNewUserData
,
alivePokemons
,
setAlivePokemons
}:
BottomBarProps
)
=>
{
const
{
userData
}
=
useLoggedInUser
();
const
pokeballData
=
useMemo
(
()
=>
getUserPokeballs
(
userData
?.
pokeballIds
),
[
userData
]
()
=>
getUserPokeballs
(
newUserData
.
pokeballIds
),
[
newUserData
]
);
const
foodData
=
useMemo
(
()
=>
getUserFood
(
newUserData
.
foodIds
),
[
newUserData
]
);
const
foodData
=
useMemo
(()
=>
getUserFood
(
userData
?.
foodIds
),
[
userData
]);
if
(
pokeballData
===
null
||
foodData
===
null
)
{
return
<
Loading
/>;
...
...
@@ -43,6 +61,7 @@ export const BottomBar = ({
food
=
{
f
}
setHearts
=
{
setHearts
}
currentHearts
=
{
currentHearts
}
setNewUserData
=
{
setNewUserData
}
style
=
{
idx
>
1
?
{
marginLeft
:
5
}
:
undefined
}
/>
))
}
...
...
@@ -50,12 +69,13 @@ export const BottomBar = ({
</
Col
>
<
Col
span
=
{
12
}
>
{
pokeballData
.
map
((
p
,
idx
,
arr
)
=>
(
<
Powerup
<
Pokeball
Powerup
key
=
{
idx
}
icon
=
{
<
TrademarkCircleOutlined
/>
}
onClick
=
{
()
=>
{
console
.
log
(
'
consume pokeball
'
,
p
);
}
}
alivePokemons
=
{
alivePokemons
}
pokeball
=
{
p
}
setAlivePokemons
=
{
setAlivePokemons
}
setNewUserData
=
{
setNewUserData
}
setScore
=
{
setScore
}
style
=
{
idx
!==
arr
.
length
-
1
?
{
marginLeft
:
5
}
:
undefined
}
/>
))
}
...
...
src/pages/Game/Canvas.tsx
View file @
410071e2
...
...
@@ -13,7 +13,11 @@ type PokemonObjectProps = {
};
const
Esh
=
()
=>
(
<
CanvasObject
className
=
"esh"
{
...
esh
}
style
=
{
{
backgroundColor
:
'
red
'
}
}
/>
<
CanvasObject
className
=
"esh"
{
...
esh
}
style
=
{
{
backgroundImage
:
`url("/ash.png")`
,
backgroundSize
:
'
cover
'
}
}
/>
);
const
PokemonObject
=
({
x
,
y
,
name
,
sprite
}:
PokemonObjectProps
)
=>
(
<
CanvasObject
...
...
src/pages/Game/FoodPowerup.tsx
View file @
410071e2
import
{
RestOutlined
}
from
'
@ant-design/icons
'
;
import
{
Badge
}
from
'
antd
'
;
import
{
maxHearts
}
from
'
../../data/constants
'
;
import
{
Food
}
from
'
../../data/food
'
;
import
{
SetUserData
}
from
'
../../utils/commonTypes
'
;
import
{
Powerup
}
from
'
./Powerup
'
;
...
...
@@ -10,13 +12,15 @@ type FoodPowerupProps = {
currentHearts
:
number
;
food
:
Food
;
style
?:
React
.
CSSProperties
;
setNewUserData
:
SetUserData
;
};
export
const
FoodPowerup
=
({
food
,
setHearts
,
currentHearts
,
style
style
,
setNewUserData
}:
FoodPowerupProps
)
=>
{
const
heal
=
(
restores
:
number
)
=>
{
const
healingEffect
=
currentHearts
+
restores
;
...
...
@@ -24,12 +28,22 @@ export const FoodPowerup = ({
};
return
(
<
Powerup
icon
=
{
<
RestOutlined
/>
}
onClick
=
{
()
=>
{
heal
(
food
.
restores
);
}
}
style
=
{
style
}
/>
<
Badge
count
=
{
food
.
restores
}
>
<
Powerup
icon
=
{
<
RestOutlined
/>
}
onClick
=
{
()
=>
{
heal
(
food
.
restores
);
setNewUserData
(
prev
=>
prev
!==
undefined
?
{
...
prev
,
foodIds
:
prev
.
foodIds
.
filter
(
id
=>
id
!==
food
.
id
)
}
:
undefined
);
}
}
style
=
{
style
}
/>
</
Badge
>
);
};
src/pages/Game/Game.tsx
View file @
410071e2
import
{
useEffect
,
useState
}
from
'
react
'
;
import
{
cloneDeep
,
set
,
union
}
from
'
lodash
'
;
import
{
useEffect
,
useLayoutEffect
,
useState
}
from
'
react
'
;
import
{
catchReward
,
maxHearts
}
from
'
../../data/constants
'
;
import
{
spawnPokemons
}
from
'
../../data/pokemons
'
;
import
{
useGameInput
}
from
'
../../hooks/useGameInput
'
;
import
{
useLoggedInUser
}
from
'
../../hooks/useLoggedInUser
'
;
import
{
SetGamePhase
,
SetPokemonsAlive
,
SetScore
,
SetUserData
}
from
'
../../utils/commonTypes
'
;
import
{
getElementSize
}
from
'
../../utils/dom
'
;
import
{
UserData
}
from
'
../../utils/firebase
'
;
import
{
updateUserDataInFirebase
,
UserData
}
from
'
../../utils/firebase
'
;
import
{
Loading
}
from
'
../../utils/Loading
'
;
import
{
PokemonOnCanvas
}
from
'
../../utils/pokemonFetcher
'
;
import
{
BottomBar
}
from
'
./BottomBar
'
;
import
{
Canvas
}
from
'
./Canvas
'
;
import
{
TopBar
}
from
'
./TopBar
'
;
import
{
onEshDeath
,
TopBar
}
from
'
./TopBar
'
;
export
type
GamePhase
=
'
play
'
|
'
prepare
'
|
'
lost
'
;
type
SetHearts
=
React
.
Dispatch
<
React
.
SetStateAction
<
number
>>
;
type
SetInput
=
React
.
Dispatch
<
React
.
SetStateAction
<
string
>>
;
type
GameProps
=
{
hearts
:
number
;
setHearts
:
React
.
Dispatch
<
React
.
SetStateAction
<
number
>>
;
pokemonsAlive
:
PokemonOnCanvas
[];
setPokemonsAlive
:
React
.
Dispatch
<
React
.
SetStateAction
<
PokemonOnCanvas
[]
>>
;
setScore
:
React
.
Dispatch
<
React
.
SetStateAction
<
number
|
undefined
>>
;
setGamePhase
:
React
.
Dispatch
<
React
.
SetStateAction
<
GamePhase
>>
;
score
?:
number
;
userData
?:
UserData
;
setGamePhase
:
SetGamePhase
;
};
const
Game
=
({
hearts
,
setHearts
,
pokemonsAlive
,
setPokemonsAlive
,
setScore
,
score
,
userData
,
setGamePhase
}:
GameProps
)
=>
{
const
onPokemonCatch
=
(
setPokemonsAlive
:
SetPokemonsAlive
,
setScore
:
SetScore
,
setNewUserData
:
SetUserData
,
setInput
:
SetInput
,
input
:
string
,
catchedIds
:
number
[]
)
=>
{
setPokemonsAlive
(
prevPokemons
=>
prevPokemons
.
filter
(
p
=>
p
.
name
!==
input
));
setScore
(
prev
=>
prev
!==
undefined
?
prev
+
catchedIds
.
length
*
catchReward
:
undefined
);
setNewUserData
(
prev
=>
prev
!==
undefined
?
{
...
prev
,
catchedPokemonIds
:
union
(
prev
.
catchedPokemonIds
,
catchedIds
)
}
:
undefined
);
setInput
(
''
);
};
const
spawnNextWawe
=
(
setPokemonsAlive
:
SetPokemonsAlive
)
=>
{
setPokemonsAlive
(
prev
=>
[...
prev
,
...
spawnPokemons
(
prev
)]);
};
const
movePokemons
=
(
pokemonsAlive
:
PokemonOnCanvas
[],
setPokemonsAlive
:
SetPokemonsAlive
,
setHearts
:
SetHearts
,
colisionBorder
?:
number
)
=>
{
if
(
colisionBorder
!==
undefined
)
{
const
pokemonsBehindBorder
=
pokemonsAlive
.
filter
(
p
=>
p
.
x
<=
colisionBorder
);
setPokemonsAlive
(
p
=>
p
.
filter
(
p
=>
!
pokemonsBehindBorder
.
map
(
pbh
=>
pbh
.
id
).
includes
(
p
.
id
))
);
setHearts
(
hearts
=>
hearts
-
pokemonsBehindBorder
.
length
>
0
?
hearts
-
pokemonsBehindBorder
.
length
:
0
);
}
setPokemonsAlive
(
prevPokemons
=>
prevPokemons
.
map
(
p
=>
({
...
p
,
x
:
p
.
x
-
0.1
}))
);
};
const
Game
=
({
setGamePhase
}:
GameProps
)
=>
{
const
{
userData
}
=
useLoggedInUser
();
const
[
hearts
,
setHearts
]
=
useState
(
maxHearts
);
const
[
score
,
setScore
]
=
useState
<
number
|
undefined
>
();
const
[
seconds
,
setSeconds
]
=
useState
(
0
);
const
[
pokemonsAlive
,
setPokemonsAlive
]
=
useState
<
PokemonOnCanvas
[]
>
(
spawnPokemons
([])
);
const
[
input
,
setInput
]
=
useGameInput
();
const
[
colisionBorder
,
setColisionBorder
]
=
useState
<
number
>
();
const
[
newUserData
,
setNewUserData
]
=
useState
<
UserData
>
();
// check if esh died
useEffect
(()
=>
{
if
(
hearts
<=
0
)
{
setGamePhase
(
'
lost
'
);
onEshDeath
(
setGamePhase
,
userData
!
,
newUserData
!
,
score
!
,
seconds
);
}
},
[
hearts
]);
},
[
hearts
,
setGamePhase
,
userData
]);
// find out esh
size
// find out esh
width
useEffect
(()
=>
{
const
canvasHeight
=
getElementSize
(
'
.canvas
'
)?.
height
;
const
esh
=
getElementSize
(
'
.esh
'
)?.
height
;
if
(
canvasHeight
!==
undefined
&&
esh
!==
undefined
)
{
setColisionBorder
((
esh
/
canvasHeight
)
*
100
);
}
if
(
userData
!==
undefined
)
{
setScore
(
userData
.
actualScore
);
const
canvasWidth
=
getElementSize
(
'
.canvas
'
)?.
width
;
const
esh
=
getElementSize
(
'
.esh
'
)?.
width
;
if
(
canvasWidth
!==
undefined
&&
esh
!==
undefined
)
{
setColisionBorder
((
esh
/
canvasWidth
)
*
100
);
}
});
// set score after userData loads
// update colision border on window resize
useLayoutEffect
(()
=>
{
const
updateColisionBorder
=
()
=>
{
const
canvasWidth
=
getElementSize
(
'
.canvas
'
)?.
width
;
const
esh
=
getElementSize
(
'
.esh
'
)?.
width
;
const
newCollisionBorder
=
esh
!==
undefined
&&
canvasWidth
!==
undefined
?
(
esh
/
canvasWidth
)
*
100
:
undefined
;
setColisionBorder
(
newCollisionBorder
);
};
window
.
addEventListener
(
'
resize
'
,
updateColisionBorder
);
updateColisionBorder
();
return
()
=>
window
.
removeEventListener
(
'
resize
'
,
updateColisionBorder
);
},
[]);
// set score after userData loads and make copy of the data
useEffect
(()
=>
{
if
(
userData
!==
undefined
)
{
setScore
(
userData
.
actualScore
);
setNewUserData
(
cloneDeep
(
userData
));
}
},
[
userData
]);
},
[
userData
,
setScore
,
setNewUserData
]);
// spawn another pokemons after some interval
useEffect
(()
=>
{
const
id
=
setInterval
(()
=>
{
console
.
log
(
'
now
'
);
setPokemonsAlive
(
prev
=>
[...
prev
,
...
spawnPokemons
(
prev
)]);
spawnNextWawe
(
setPokemonsAlive
);
},
3000
);
return
()
=>
clearInterval
(
id
);
},
[]);
//count seconds
useEffect
(()
=>
{
const
id
=
setInterval
(()
=>
{
setSeconds
(
prev
=>
prev
+
1
);
},
1000
);
return
()
=>
clearInterval
(
id
);
},
[]);
// move pokemons
useEffect
(()
=>
{
const
id
=
setInterval
(()
=>
{
if
(
colisionBorder
!==
undefined
)
{
const
pokemonsBehindBorder
=
pokemonsAlive
.
filter
(
p
=>
p
.
x
<=
colisionBorder
);
setPokemonsAlive
(
p
=>
p
.
filter
(
p
=>
!
pokemonsBehindBorder
.
map
(
pbh
=>
pbh
.
id
).
includes
(
p
.
id
))
);
setHearts
(
hearts
=>
hearts
-
pokemonsBehindBorder
.
length
>
0
?
hearts
-
pokemonsBehindBorder
.
length
:
0
);
}
setPokemonsAlive
(
prevPokemons
=>
prevPokemons
.
map
(
p
=>
({
...
p
,
x
:
p
.
x
-
0.1
}))
);
movePokemons
(
pokemonsAlive
,
setPokemonsAlive
,
setHearts
,
colisionBorder
);
},
20
);
return
()
=>
clearInterval
(
id
);
},
[
pokemonsAlive
]);
},
[
pokemonsAlive
,
setPokemonsAlive
,
setHearts
,
colisionBorder
]);
const
tryToCatchPokemon
=
(
e
:
KeyboardEvent
)
=>
{
const
enterKeyCode
=
13
;
if
(
e
.
keyCode
===
enterKeyCode
)
{
setPokemonsAlive
(
prevPokemons
=>
prevPokemons
.
filter
(
p
=>
p
.
name
!==
input
)
const
catchedIds
=
pokemonsAlive
.
filter
(
p
=>
p
.
name
===
input
)
.
map
(
p
=>
p
.
id
);
onPokemonCatch
(
setPokemonsAlive
,
setScore
,
setNewUserData
,
setInput
,
input
,
catchedIds
);
setInput
(
''
);
}
else
{
setInput
(
input
=>
input
.
concat
(
e
.
key
));
}
...
...
@@ -121,15 +187,34 @@ const Game = ({
};
},
[
input
,
setPokemonsAlive
,
setInput
]);
if
(
score
===
undefined
)
{
if
(
score
===
undefined
||
newUserData
===
undefined
||
userData
===
undefined
)
{
return
<
Loading
/>;
}
return
(
<>
<
TopBar
hearts
=
{
hearts
}
score
=
{
score
}
/>
<
TopBar
hearts
=
{
hearts
}
score
=
{
score
}
seconds
=
{
seconds
}
userData
=
{
userData
}
newUserData
=
{
newUserData
}
setGamePhase
=
{
setGamePhase
}
/>
<
Canvas
pokemonsAlive
=
{
pokemonsAlive
}
/>
<
BottomBar
currentHearts
=
{
hearts
}
setHearts
=
{
setHearts
}
/>
<
BottomBar
currentHearts
=
{
hearts
}
setScore
=
{
setScore
}
setHearts
=
{
setHearts
}
newUserData
=
{
newUserData
}
setNewUserData
=
{
setNewUserData
}
alivePokemons
=
{
pokemonsAlive
}
setAlivePokemons
=
{
setPokemonsAlive
}
/>
</>
);
};
...
...
src/pages/Game/GameLostModal.tsx
View file @
410071e2
import
{
FrownOutlined
,
RocketOutlined
}
from
'
@ant-design/icons
'
;
import
{
Modal
,
Typography
}
from
'
antd
'
;
import
{
useHistory
}
from
'
react-router-dom
'
;
...
...
@@ -12,9 +13,17 @@ export const GameLostModal = ({ visible, onOk }: GameLostModalProps) => {
<
Modal
title
=
{
null
}
visible
=
{
visible
}
okText
=
"Sure"
okText
=
{
<
span
>
<
RocketOutlined
/>
Bring it on
</
span
>
}
onOk
=
{
onOk
}
cancelText
=
"Nope, show me stats"
cancelText
=
{
<
span
>
<
FrownOutlined
/>
Nope, show me stats
</
span
>
}
onCancel
=
{
()
=>
{
push
(
'
/leaderboard
'
);
setSelectedMenuKey
(
'
leaderboard
'
);
...
...
src/pages/Game/GamePage.tsx
View file @
410071e2
import
{
useCallback
,
useState
}
from
'
react
'
;
import
{
useState
}
from
'
react
'
;
import
{
maxHearts
}
from
'
../../data/constants
'
;
import
{
spawnPokemons
}
from
'
../../data/pokemons
'
;
import
{
useLoggedInUser
}
from
'
../../hooks/useLoggedInUser
'
;
import
usePageTitle
from
'
../../hooks/usePageTitle
'
;
import
{
PokemonOnCanv
as
}
from
'
../../utils/
pokemonFetcher
'
;
import
{
GamePh
as
e
}
from
'
../../utils/
commonTypes
'
;
import
Game
,
{
GamePhase
}
from
'
./Game
'
;
import
Game
from
'
./Game
'
;
import
{
GameLostModal
}
from
'
./GameLostModal
'
;
import
{
RulesModal
}
from
'
./RulesModal
'
;
const
GamePage
=
()
=>
{
usePageTitle
(
'
Game
'
);
const
{
userData
}
=
useLoggedInUser
();
const
[
hearts
,
setHearts
]
=
useState
(
maxHearts
);
const
[
score
,
setScore
]
=
useState
<
number
|
undefined
>
();