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
07aa1272
Unverified
Commit
07aa1272
authored
Nov 11, 2021
by
Michal Čaniga
Browse files
Add game layout
parent
aa46a8a3
Changes
18
Hide whitespace changes
Inline
Side-by-side
src/App.tsx
View file @
07aa1272
...
...
@@ -25,14 +25,25 @@ import Leaderboard from './pages/Leaderboard/Leaderboard';
import
Store
from
'
./pages/Store
'
;
import
{
signOut
}
from
'
./utils/firebase
'
;
import
useLoggedInUser
from
'
./hooks/useLoggedInUser
'
;
import
{
MenuKey
,
SelectedMenuKeyProvider
,
useSelectedMenuKey
}
from
'
./hooks/useMenu
'
;
import
{
GameInputProvider
}
from
'
./hooks/useGameInput
'
;
const
TopMenu
=
()
=>
{
const
currentUser
=
useLoggedInUser
();
const
isLoggedIn
=
useMemo
(()
=>
currentUser
!==
undefined
,
[
currentUser
]);
const
{
push
}
=
useHistory
();
const
[
selectedMenuKey
,
setSelectedMenuKey
]
=
useSelectedMenuKey
();
return
(
<
Menu
style
=
{
{
padding
:
'
0 px
'
,
height
:
'
5%
'
}
}
mode
=
"horizontal"
>
<
Menu
onClick
=
{
e
=>
setSelectedMenuKey
(
e
.
key
as
MenuKey
)
}
selectedKeys
=
{
[
selectedMenuKey
]
}
style
=
{
{
padding
:
'
0 px
'
,
height
:
'
5%
'
}
}
mode
=
"horizontal"
>
<
Menu
.
Item
key
=
"home"
icon
=
{
<
HomeOutlined
/>
}
>
<
Link
to
=
"/"
>
<
Button
type
=
"text"
>
Home
</
Button
>
...
...
@@ -66,6 +77,7 @@ const TopMenu = () => {
onClick
=
{
async
()
=>
{
await
signOut
();
push
(
'
/
'
);
setSelectedMenuKey
(
'
home
'
);
}
}
>
Logout
...
...
@@ -100,8 +112,12 @@ const AppContent = () => {
const
App
=
()
=>
(
<
BrowserRouter
>
<
TopMenu
/>
<
AppContent
/>
<
SelectedMenuKeyProvider
>
<
GameInputProvider
>
<
TopMenu
/>
<
AppContent
/>
</
GameInputProvider
>
</
SelectedMenuKeyProvider
>
</
BrowserRouter
>
);
...
...
src/data/constants.ts
View file @
07aa1272
export
const
maxHearts
=
5
;
export
const
pokemonSampleCount
=
20
;
export
const
pokemonSampleCount
=
2
;
export
const
esh
=
{
x
:
0
,
y
:
40
,
width
:
300
};
export
const
eshXColisionBorder
=
esh
.
x
+
esh
.
width
;
export
const
pokemonWidth
=
100
;
export
const
pokemonSpawn
=
{
x
:
120
,
y
:
[
40
,
60
]
};
src/data/pokemons.ts
View file @
07aa1272
...
...
@@ -3,13 +3,19 @@ import { sampleSize } from 'lodash';
import
{
getDataByIds
}
from
'
../utils/dataUtils
'
;
import
{
Pokemon
,
PokemonOnCanvas
}
from
'
../utils/pokemonFetcher
'
;
import
{
pokemonSampleCount
}
from
'
./constants
'
;
import
{
pokemonSampleCount
,
pokemonSpawn
}
from
'
./constants
'
;
export
const
getCatchedPokemons
=
(
catchedPokemonIds
:
number
[]):
Pokemon
[]
=>
getDataByIds
(
pokemons
,
catchedPokemonIds
);
export
const
getPokemonsSample
=
():
PokemonOnCanvas
[]
=>
sampleSize
(
pokemons
,
pokemonSampleCount
).
map
(
p
=>
({
...
p
,
x
:
0
,
y
:
0
}));
// TODO: determine better starting position
export
const
spawnPokemons
=
():
PokemonOnCanvas
[]
=>
{
const
[
first
,
second
]
=
sampleSize
(
pokemons
,
pokemonSampleCount
);
const
{
x
,
y
}
=
pokemonSpawn
;
return
[
{
...
first
,
x
,
y
:
y
[
0
]
},
{
...
second
,
x
,
y
:
y
[
1
]
}
];
};
// data is saved result of getPokemons obtained from src/pokemonFetcher.ts
export
const
pokemons
:
Pokemon
[]
=
[
...
...
src/hooks/useGameInput.tsx
0 → 100644
View file @
07aa1272
import
{
createContext
,
Dispatch
,
FC
,
SetStateAction
,
useContext
,
useState
}
from
'
react
'
;
type
GameInputState
=
[
string
,
Dispatch
<
SetStateAction
<
string
>>
];
const
GameInputContext
=
createContext
<
GameInputState
>
(
undefined
as
never
);
export
const
GameInputProvider
:
FC
=
({
children
})
=>
{
const
gameInput
=
useState
<
string
>
(
''
);
return
(
<
GameInputContext
.
Provider
value
=
{
gameInput
}
>
{
children
}
</
GameInputContext
.
Provider
>
);
};
export
const
useGameInput
=
()
=>
useContext
(
GameInputContext
);
src/hooks/useLoggedInUser.ts
View file @
07aa1272
...
...
@@ -4,6 +4,7 @@ import { User } from 'firebase/auth';
import
{
onAuthChanged
}
from
'
../utils/firebase
'
;
// Hook providing logged in user information
// TODO: rewrite to context
const
useLoggedInUser
=
()
=>
{
// Hold user info in state
const
[
user
,
setUser
]
=
useState
<
User
>
();
...
...
src/hooks/useLoggedInUserData.ts
View file @
07aa1272
...
...
@@ -20,7 +20,7 @@ const useLoggedInUserData = () => {
return
()
=>
{
unsubscribe
();
};
},
[]);
},
[
currentUser
]);
return
userData
;
};
...
...
src/hooks/useMenu.tsx
0 → 100644
View file @
07aa1272
import
{
createContext
,
Dispatch
,
FC
,
SetStateAction
,
useContext
,
useState
}
from
'
react
'
;
export
type
MenuKey
=
|
'
home
'
|
'
game
'
|
'
store
'
|
'
encyclopedia
'
|
'
leaderboard
'
|
'
logout
'
;
type
MenuState
=
[
MenuKey
,
Dispatch
<
SetStateAction
<
MenuKey
>>
];
const
MenuContext
=
createContext
<
MenuState
>
(
undefined
as
never
);
export
const
SelectedMenuKeyProvider
:
FC
=
({
children
})
=>
{
const
selectedMenuKeyState
=
useState
<
MenuKey
>
(
'
home
'
);
return
(
<
MenuContext
.
Provider
value
=
{
selectedMenuKeyState
}
>
{
children
}
</
MenuContext
.
Provider
>
);
};
export
const
useSelectedMenuKey
=
()
=>
useContext
(
MenuContext
);
src/pages/Game/BottomBar.tsx
View file @
07aa1272
...
...
@@ -5,6 +5,7 @@ import { useMemo } from 'react';
import
{
getUserFood
}
from
'
../../data/food
'
;
import
{
getUserPokeballs
}
from
'
../../data/pokeballs
'
;
import
useLoggedInUserData
from
'
../../hooks/useLoggedInUserData
'
;
import
{
Loading
}
from
'
../../utils/Loading
'
;
import
{
FoodPowerup
}
from
'
./FoodPowerup
'
;
import
{
Powerup
}
from
'
./Powerup
'
;
...
...
@@ -28,7 +29,7 @@ export const BottomBar = ({
const
foodData
=
useMemo
(()
=>
getUserFood
(
userData
?.
foodIds
),
[
userData
]);
if
(
pokeballData
===
null
||
foodData
===
null
)
{
return
null
;
return
<
Loading
/>
;
}
return
(
...
...
src/pages/Game/Canvas.tsx
View file @
07aa1272
import
{
esh
,
pokemonWidth
}
from
'
../../data/constants
'
;
import
{
PokemonOnCanvas
}
from
'
../../utils/pokemonFetcher
'
;
type
CanvasProps
=
{
pokemonsAlive
:
PokemonOnCanvas
[];
};
type
PokemonObjectProps
=
{
x
:
number
;
y
:
number
;
name
:
string
;
};
const
Esh
=
()
=>
<
CanvasObject
{
...
esh
}
style
=
{
{
backgroundColor
:
'
red
'
}
}
/>;
const
PokemonObject
=
({
x
,
y
,
name
}:
PokemonObjectProps
)
=>
(
<
CanvasObject
x
=
{
x
}
y
=
{
y
}
width
=
{
pokemonWidth
}
style
=
{
{
backgroundColor
:
'
blue
'
}
}
label
=
{
name
}
/>
);
type
CanvasObjectProps
=
{
x
:
number
;
y
:
number
;
width
:
number
;
label
?:
string
;
style
?:
React
.
CSSProperties
;
};
const
CanvasObject
=
({
x
,
y
,
width
,
style
=
{},
label
}:
CanvasObjectProps
)
=>
(
<
div
style
=
{
{
paddingTop
:
`
${
width
}
px`
,
width
:
`
${
width
}
px`
,
position
:
'
absolute
'
,
left
:
`
${
x
}
%`
,
bottom
:
`
${
y
}
%`
,
textAlign
:
'
center
'
,
...
style
}
}
>
{
label
!==
undefined
?
(
<
span
style
=
{
{
position
:
'
absolute
'
,
bottom
:
65
,
width
:
'
100%
'
,
height
:
'
100%
'
,
display
:
'
flex
'
,
alignItems
:
'
center
'
,
justifyContent
:
'
center
'
}
}
>
{
label
}
</
span
>
)
:
null
}
</
div
>
);
export
const
Canvas
=
({
pokemonsAlive
}:
CanvasProps
)
=>
(
<
div
style
=
{
{
height
:
'
85%
'
}
}
>
Canvas
</
div
>
<
div
style
=
{
{
height
:
'
85%
'
,
position
:
'
relative
'
}
}
>
<
Esh
/>
{
pokemonsAlive
.
map
((
p
,
idx
)
=>
(
<
PokemonObject
key
=
{
idx
}
x
=
{
p
.
x
}
y
=
{
p
.
y
}
name
=
{
p
.
name
}
/>
))
}
</
div
>
);
src/pages/Game/Game.tsx
View file @
07aa1272
import
{
useEffect
,
useMemo
,
useState
}
from
'
react
'
;
import
{
useCallback
,
useEffect
,
useMemo
,
useState
}
from
'
react
'
;
import
{
maxHearts
}
from
'
../../data/constants
'
;
import
{
getPokemonsSample
}
from
'
../../data/pokemons
'
;
import
{
eshXColisionBorder
,
maxHearts
}
from
'
../../data/constants
'
;
import
{
spawnPokemons
}
from
'
../../data/pokemons
'
;
import
{
useGameInput
}
from
'
../../hooks/useGameInput
'
;
import
useLoggedInUserData
from
'
../../hooks/useLoggedInUserData
'
;
import
usePageTitle
from
'
../../hooks/usePageTitle
'
;
import
{
Loading
}
from
'
../../utils/Loading
'
;
import
{
PokemonOnCanvas
}
from
'
../../utils/pokemonFetcher
'
;
import
{
BottomBar
}
from
'
./BottomBar
'
;
...
...
@@ -19,16 +21,16 @@ const Game = () => {
const
[
hearts
,
setHearts
]
=
useState
(
maxHearts
);
const
[
score
,
setScore
]
=
useState
<
number
|
undefined
>
();
const
[
pokemonsAlive
,
setPokemonsAlive
]
=
useState
<
PokemonOnCanvas
[]
>
(
get
Pokemons
Sample
()
spawn
Pokemons
()
);
const
[
gameStarted
,
setGameStarted
]
=
useState
(
false
);
const
[
input
,
setInput
]
=
use
State
(
''
);
const
[
input
,
setInput
]
=
use
GameInput
(
);
const
resetGame
=
()
=>
{
const
resetGame
=
useCallback
(
()
=>
{
setHearts
(
maxHearts
);
setScore
(
userData
?.
actualScore
);
setPokemonsAlive
(
get
Pokemons
Sample
());
};
setPokemonsAlive
(
spawn
Pokemons
());
}
,
[
maxHearts
,
userData
])
;
const
gameLost
=
useMemo
(()
=>
hearts
<
0
,
[
hearts
]);
...
...
@@ -42,31 +44,52 @@ const Game = () => {
// get new sample when pokemons are killed
useEffect
(()
=>
{
if
(
pokemonsAlive
.
length
===
0
)
{
setPokemonsAlive
(
get
Pokemons
Sample
());
setPokemonsAlive
(
spawn
Pokemons
());
}
},
[
pokemonsAlive
]);
const
tryPokemonCatch
=
(
e
:
KeyboardEvent
)
=>
{
const
newInput
=
input
.
concat
(
e
.
key
);
// move pokemons
useEffect
(()
=>
{
const
id
=
setInterval
(()
=>
{
const
heartsLost
=
pokemonsAlive
.
filter
(
p
=>
p
.
x
<=
eshXColisionBorder
).
length
;
setHearts
(
hearts
=>
(
hearts
-
heartsLost
>
0
?
hearts
-
heartsLost
:
0
));
setPokemonsAlive
(
prevPokemons
=>
prevPokemons
.
map
(
p
=>
({
...
p
,
x
:
p
.
x
-
0.1
}))
);
},
500
);
return
()
=>
clearInterval
(
id
);
},
[
pokemonsAlive
]);
const
tryToCatchPokemon
=
(
e
:
KeyboardEvent
)
=>
{
const
enterKeyCode
=
13
;
if
(
e
.
keyCode
===
enterKeyCode
)
{
setPokemonsAlive
(
pokemonsAlive
.
filter
(
p
=>
p
.
name
!==
newInput
));
setPokemonsAlive
(
prevPokemons
=>
prevPokemons
.
filter
(
p
=>
p
.
name
!==
input
)
);
setInput
(
''
);
}
else
{
setInput
(
newInput
);
setInput
(
input
=>
input
.
concat
(
e
.
key
)
);
}
};
console
.
log
(
'
input
'
,
input
);
// Listen for user input, try to catch the pokemon
useEffect
(()
=>
{
document
.
addEventListener
(
'
keydown
'
,
tryPokemon
Catch
);
document
.
addEventListener
(
'
keydown
'
,
try
ToCatch
Pokemon
);
return
()
=>
{
document
.
removeEventListener
(
'
keydown
'
,
tryPokemon
Catch
);
document
.
removeEventListener
(
'
keydown
'
,
try
ToCatch
Pokemon
);
};
},
[]);
},
[
input
,
setPokemonsAlive
,
setInput
]);
if
(
score
===
undefined
)
{
return
null
;
return
<
Loading
/>
;
}
return
(
...
...
src/pages/Game/GameLostModal.tsx
View file @
07aa1272
import
{
Modal
,
Typography
}
from
'
antd
'
;
import
{
useHistory
}
from
'
react-router-dom
'
;
import
{
useSelectedMenuKey
}
from
'
../../hooks/useMenu
'
;
type
GameLostModalProps
=
{
visible
:
boolean
;
onOk
:
()
=>
void
};
export
const
GameLostModal
=
({
visible
,
onOk
}:
GameLostModalProps
)
=>
{
const
{
push
}
=
useHistory
();
const
[,
setSelectedMenuKey
]
=
useSelectedMenuKey
();
return
(
<
Modal
title
=
{
null
}
...
...
@@ -12,7 +15,10 @@ export const GameLostModal = ({ visible, onOk }: GameLostModalProps) => {
okText
=
"Sure"
onOk
=
{
onOk
}
cancelText
=
"Nope, show me stats"
onCancel
=
{
()
=>
push
(
'
/leaderboard
'
)
}
onCancel
=
{
()
=>
{
push
(
'
/leaderboard
'
);
setSelectedMenuKey
(
'
leaderboard
'
);
}
}
>
<
Typography
>
You have been killed, try again?
</
Typography
>
</
Modal
>
...
...
src/pages/Game/Lives.tsx
View file @
07aa1272
import
{
HeartOutlined
}
from
'
@ant-design/icons
'
;
import
{
Rate
}
from
'
antd
'
;
import
{
maxHearts
}
from
'
../../data/constants
'
;
...
...
@@ -7,13 +6,19 @@ type HeartsProps = {
hearts
:
number
;
};
const
style
=
{
color
:
'
red
'
,
margin
:
'
10px 0px 0px 10px
'
,
fontSize
:
25
};
const
heartNumbers
=
Array
.
from
({
length
:
maxHearts
},
(
_
,
i
)
=>
i
+
1
);
const
Lives
=
({
hearts
}:
HeartsProps
)
=>
(
<
Rate
defaultValue
=
{
hearts
}
count
=
{
maxHearts
}
disabled
character
=
{
<
HeartOutlined
/>
}
/>
<
div
>
{
heartNumbers
.
map
(
h
=>
h
>
hearts
?
(
<
HeartOutlined
style
=
{
{
...
style
,
color
:
'
gray
'
}
}
/>
)
:
(
<
HeartOutlined
style
=
{
style
}
/>
)
)
}
</
div
>
);
export
default
Lives
;
src/pages/Game/RulesModal.tsx
View file @
07aa1272
import
{
Modal
,
Typography
}
from
'
antd
'
;
import
{
useHistory
}
from
'
react-router-dom
'
;
import
{
useSelectedMenuKey
}
from
'
../../hooks/useMenu
'
;
type
RulesModalProps
=
{
visible
:
boolean
;
onOk
:
()
=>
void
};
export
const
RulesModal
=
({
visible
,
onOk
}:
RulesModalProps
)
=>
{
const
{
push
}
=
useHistory
();
const
[,
setSelectedMenuKey
]
=
useSelectedMenuKey
();
return
(
<
Modal
title
=
"Game rules"
...
...
@@ -12,7 +15,10 @@ export const RulesModal = ({ visible, onOk }: RulesModalProps) => {
okText
=
"I Understand, let's go"
onOk
=
{
onOk
}
cancelText
=
"Hell no"
onCancel
=
{
()
=>
push
(
'
/
'
)
}
onCancel
=
{
()
=>
{
push
(
'
/
'
);
setSelectedMenuKey
(
'
home
'
);
}
}
>
<
Typography
>
Rule 1
</
Typography
>
<
Typography
>
Rule 2
</
Typography
>
...
...
src/pages/Game/Score.tsx
View file @
07aa1272
import
{
MoneyCollect
Outlined
}
from
'
@ant-design/icons
'
;
import
{
Dollar
Outlined
}
from
'
@ant-design/icons
'
;
import
{
Statistic
}
from
'
antd
'
;
type
ScoreProps
=
{
...
...
@@ -6,7 +6,11 @@ type ScoreProps = {
};
const
Score
=
({
score
}:
ScoreProps
)
=>
(
<
Statistic
value
=
{
score
}
prefix
=
{
<
MoneyCollectOutlined
/>
}
/>
<
Statistic
groupSeparator
=
{
'
'
}
value
=
{
score
}
suffix
=
{
<
DollarOutlined
style
=
{
{
color
:
'
#ffd700
'
}
}
/>
}
/>
);
export
default
Score
;
src/pages/Game/TopBar.tsx
View file @
07aa1272
import
{
Col
,
Row
}
from
'
antd
'
;
import
{
Col
,
Row
,
Typography
}
from
'
antd
'
;
import
{
useGameInput
}
from
'
../../hooks/useGameInput
'
;
import
Lives
from
'
./Lives
'
;
import
Score
from
'
./Score
'
;
...
...
@@ -8,15 +10,23 @@ type TopBarProps = {
score
:
number
;
};
export
const
TopBar
=
({
hearts
,
score
}:
TopBarProps
)
=>
(
<
Row
justify
=
"space-between"
>
<
Col
span
=
{
12
}
>
<
Score
score
=
{
score
}
/>
</
Col
>
<
Col
span
=
{
12
}
>
<
div
style
=
{
{
display
:
'
flex
'
,
justifyContent
:
'
flex-end
'
}
}
>
<
Lives
hearts
=
{
hearts
}
/>
</
div
>
</
Col
>
</
Row
>
);
export
const
TopBar
=
({
hearts
,
score
}:
TopBarProps
)
=>
{
const
[
input
]
=
useGameInput
();
return
(
<
Row
justify
=
"space-between"
>
<
Col
span
=
{
8
}
>
<
Score
score
=
{
score
}
/>
</
Col
>
<
Col
span
=
{
8
}
>
<
div
style
=
{
{
display
:
'
flex
'
,
justifyContent
:
'
center
'
}
}
>
<
span
style
=
{
{
fontWeight
:
'
bold
'
,
fontSize
:
30
}
}
>
{
input
}
</
span
>
</
div
>
</
Col
>
<
Col
span
=
{
8
}
>
<
div
style
=
{
{
display
:
'
flex
'
,
justifyContent
:
'
flex-end
'
}
}
>
<
Lives
hearts
=
{
hearts
}
/>
</
div
>
</
Col
>
</
Row
>
);
};
src/pages/Home/AuthModal.tsx
View file @
07aa1272
...
...
@@ -12,6 +12,7 @@ import { useCallback } from 'react';
import
{
ErrorMessage
}
from
'
@hookform/error-message
'
;
import
{
signIn
,
signUp
}
from
'
../../utils/firebase
'
;
import
{
Loading
}
from
'
../../utils/Loading
'
;
type
AuthModalProps
=
{
type
?:
AuthModalType
;
...
...
@@ -79,7 +80,6 @@ export const AuthModal = ({
const
onOk
=
useCallback
(
async
({
email
,
password
}:
FormInput
)
=>
{
console
.
log
(
type
);
try
{
switch
(
type
)
{
case
AuthModalType
.
SignUp
:
...
...
@@ -100,7 +100,7 @@ export const AuthModal = ({
);
if
(
type
===
undefined
||
title
===
undefined
)
{
return
null
;
return
<
Loading
/>
;
}
return
(
...
...
src/pages/Leaderboard/Leaderboard.tsx
View file @
07aa1272
import
{
Table
}
from
'
antd
'
;
import
{
Spin
,
Table
}
from
'
antd
'
;
import
{
SortOrder
}
from
'
antd/lib/table/interface
'
;
import
{
User
}
from
'
firebase/auth
'
;
import
{
useMemo
,
useState
}
from
'
react
'
;
...
...
@@ -9,6 +9,7 @@ import { UserData } from '../../utils/firebase';
import
'
./Leaderboard.css
'
;
import
{
makeStringSorter
,
makeNumberSorter
}
from
'
../../utils/sorters
'
;
import
useAllUserData
from
'
../../hooks/useAllUserData
'
;
import
{
Loading
}
from
'
../../utils/Loading
'
;
import
{
LeaderboardData
,
...
...
@@ -93,7 +94,7 @@ const Leaderboard = () => {
leaderboardData
===
null
||
currentUserData
===
null
)
{
return
null
;
return
<
Loading
/>
;
}
return
(
...
...
src/utils/Loading.tsx
0 → 100644
View file @
07aa1272
import
{
MehOutlined
}
from
'
@ant-design/icons
'
;
import
{
Spin
}
from
'
antd
'
;
export
const
Loading
=
()
=>
(
<
div
style
=
{
{
height
:
'
100%
'
,
display
:
'
flex
'
,
justifyContent
:
'
center
'
,
alignItems
:
'
center
'
}
}
>
<
Spin
size
=
"large"
indicator
=
{
<
MehOutlined
style
=
{
{
fontSize
:
120
}
}
spin
/>
}
/>
</
div
>
);
Write
Preview
Supports
Markdown
0%
Try again
or
attach a new file
.
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment