Commit 1fee183d authored by Michal Čaniga's avatar Michal Čaniga
Browse files

Merge branch 'feature/game-layout' into 'master'

Feature/game layout

See merge request !1
parents 1caeac61 3f3374d8
......@@ -5,4 +5,8 @@ html, body {
margin:0;
padding:0;
height:100%;
}
#root{
height:100%;
}
\ No newline at end of file
......@@ -19,20 +19,32 @@ const { Content } = Layout;
import { useMemo } from 'react';
import Encyclopedia from './pages/Encyclopedia';
import Game from './pages/Game';
import Home from './pages/Home/Home';
import Leaderboard from './pages/Leaderboard/Leaderboard';
import Store from './pages/Store';
import { signOut } from './utils/firebase';
import useLoggedInUser from './hooks/useLoggedInUser';
import { useLoggedInUser, UserProvider } from './hooks/useLoggedInUser';
import {
MenuKey,
SelectedMenuKeyProvider,
useSelectedMenuKey
} from './hooks/useMenu';
import { GameInputProvider } from './hooks/useGameInput';
import { AllUserDataProvider } from './hooks/useAllUserData';
import GamePage from './pages/Game/GamePage';
const TopMenu = () => {
const currentUser = useLoggedInUser();
const isLoggedIn = useMemo(() => currentUser !== undefined, [currentUser]);
const { user } = useLoggedInUser();
const isLoggedIn = useMemo(() => user !== undefined, [user]);
const { push } = useHistory();
const [selectedMenuKey, setSelectedMenuKey] = useSelectedMenuKey();
return (
<Menu style={{ padding: '0 px' }} 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 +78,7 @@ const TopMenu = () => {
onClick={async () => {
await signOut();
push('/');
setSelectedMenuKey('home');
}}
>
Logout
......@@ -78,11 +91,11 @@ const TopMenu = () => {
};
const AppContent = () => {
const currentUser = useLoggedInUser();
const isLoggedIn = useMemo(() => currentUser !== undefined, [currentUser]);
const { user } = useLoggedInUser();
const isLoggedIn = useMemo(() => user !== undefined, [user]);
return (
<Content style={{ padding: '0 50px' }}>
<Content style={{ padding: '0 50px', height: '95%' }}>
<Switch>
<Route path="/" exact component={Home} />
{isLoggedIn && (
......@@ -90,7 +103,7 @@ const AppContent = () => {
<Route path="/store" exact component={Store} />
<Route path="/encyclopedia" exact component={Encyclopedia} />
<Route path="/leaderboard" exact component={Leaderboard} />
<Route path="/game" exact component={Game} />
<Route path="/game" exact component={GamePage} />
</>
)}
</Switch>
......@@ -100,8 +113,16 @@ const AppContent = () => {
const App = () => (
<BrowserRouter>
<TopMenu />
<AppContent />
<SelectedMenuKeyProvider>
<GameInputProvider>
<UserProvider>
<AllUserDataProvider>
<TopMenu />
<AppContent />
</AllUserDataProvider>
</UserProvider>
</GameInputProvider>
</SelectedMenuKeyProvider>
</BrowserRouter>
);
......
export const maxHearts = 5;
export const pokemonSampleCount = 2;
export const esh = {
x: 0,
y: 40,
width: 300
};
export const pokemonWidth = 100;
export const pokemonSpawn = {
x: 101,
y: [40, 60]
};
......@@ -23,5 +23,5 @@ export const food: Food[] = [
}
];
export const getUserFood = (foodIds: number[]): Food[] =>
getDataByIds(food, foodIds);
export const getUserFood = (foodIds?: number[]): Food[] | null =>
foodIds !== undefined ? getDataByIds(food, foodIds) : null;
import { getDataByIds } from '../utils/dataUtils';
export type Pokeballs = {
export type Pokeball = {
id: number;
name: string;
price: number;
......@@ -8,11 +8,11 @@ export type Pokeballs = {
image?: string;
};
export const pokeballs: Pokeballs[] = [
export const pokeballs: Pokeball[] = [
{ id: 1, name: 'Pokeball 1', price: 12, catches: 1 },
{ id: 2, name: 'Pokeball 2', price: 48, catches: 2 },
{ id: 3, name: 'Pokeball 3', price: 19, catches: 3 }
];
export const getUserPokeballs = (pokeballIds: number[]): Pokeballs[] =>
getDataByIds(pokeballs, pokeballIds);
export const getUserPokeballs = (pokeballIds?: number[]): Pokeball[] | null =>
pokeballIds !== undefined ? getDataByIds(pokeballs, pokeballIds) : null;
import { sampleSize } from 'lodash';
import { getDataByIds } from '../utils/dataUtils';
import { Pokemon } from '../utils/pokemonFetcher';
import { Pokemon, PokemonOnCanvas } from '../utils/pokemonFetcher';
import { pokemonSampleCount, pokemonSpawn } from './constants';
export const getCatchedPokemons = (catchedPokemonIds: number[]): Pokemon[] =>
getDataByIds(pokemons, catchedPokemonIds);
export const spawnPokemons = (
pokemonsAlive: PokemonOnCanvas[]
): PokemonOnCanvas[] => {
const pokemonsNotAlreadyOnCanvas = pokemons.filter(
p => !pokemonsAlive.map(pa => pa.id).includes(p.id)
);
const [first, second] = sampleSize(
pokemonsNotAlreadyOnCanvas,
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[] = [
{
......
import { onSnapshot } from '@firebase/firestore';
import { useEffect, useState } from 'react';
import { useEffect, useState, createContext, FC, useContext } from 'react';
import { UserData, userDataCollection } from '../utils/firebase';
const useAllUserData = () => {
const AllUserDataContext = createContext<UserData[] | undefined>(undefined);
export const AllUserDataProvider: FC = ({ children }) => {
const [allUserData, setAllUserData] = useState<UserData[]>();
useEffect(() => {
......@@ -15,7 +17,13 @@ const useAllUserData = () => {
};
}, []);
return allUserData;
return (
<AllUserDataContext.Provider value={allUserData}>
{children}
</AllUserDataContext.Provider>
);
};
export const useAllUserData = () => useContext(AllUserDataContext);
export default useAllUserData;
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);
import { useEffect, useState } from 'react';
import { User } from 'firebase/auth';
import { onAuthChanged } from '../utils/firebase';
// Hook providing logged in user information
const useLoggedInUser = () => {
// Hold user info in state
const [user, setUser] = useState<User>();
// Setup onAuthChanged once when component is mounted
useEffect(() => {
onAuthChanged(u => setUser(u ?? undefined));
}, []);
return user;
};
export default useLoggedInUser;
import { createContext, FC, useContext, useState, useEffect } from 'react';
import { User } from 'firebase/auth';
import { onSnapshot } from 'firebase/firestore';
import { onAuthChanged, UserData, userDataCollection } from '../utils/firebase';
export type UserContextType = {
user?: User;
userData?: UserData;
};
const UserContext = createContext<UserContextType>({});
export const UserProvider: FC = ({ children }) => {
const [user, setUser] = useState<User>();
const [userData, setUserData] = useState<UserData>();
useEffect(() => {
onAuthChanged(u => setUser(u ?? undefined));
}, []);
useEffect(() => {
const unsubscribe = onSnapshot(userDataCollection, snapshot => {
if (user === undefined) {
return;
}
const allUserData = snapshot.docs.map(doc => doc.data());
setUserData(allUserData.filter(ud => ud.userId === user.uid)[0]);
});
return () => {
unsubscribe();
};
}, [user]);
return (
<UserContext.Provider value={{ user, userData }}>
{children}
</UserContext.Provider>
);
};
export const useLoggedInUser = () => useContext(UserContext);
import { onSnapshot } from '@firebase/firestore';
import { useEffect, useState } from 'react';
import { UserData, userDataCollection } from '../utils/firebase';
import useLoggedInUser from './useLoggedInUser';
const useLoggedInUserData = () => {
const currentUser = useLoggedInUser();
const [userData, setUserData] = useState<UserData>();
useEffect(() => {
const unsubscribe = onSnapshot(userDataCollection, snapshot => {
if (currentUser === undefined) {
return;
}
const allUserData = snapshot.docs.map(doc => doc.data());
setUserData(allUserData.filter(ud => ud.userId === currentUser.uid)[0]);
});
return () => {
unsubscribe();
};
}, []);
return userData;
};
export default useLoggedInUserData;
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);
import { Typography } from 'antd';
import usePageTitle from '../hooks/usePageTitle';
const Game = () => {
usePageTitle('Game');
return <Typography>Game</Typography>;
};
export default Game;
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 { Loading } from '../../utils/Loading';
import { FoodPowerup } from './FoodPowerup';
import { Powerup } from './Powerup';
type BottomBarProps = {
style?: React.CSSProperties;
setHearts: (hearts: number) => void;
currentHearts: number;
};
export const BottomBar = ({
style,
setHearts,
currentHearts
}: BottomBarProps) => {
const { userData } = useLoggedInUser();
const pokeballData = useMemo(
() => getUserPokeballs(userData?.pokeballIds),
[userData]
);
const foodData = useMemo(() => getUserFood(userData?.foodIds), [userData]);
if (pokeballData === null || foodData === null) {
return <Loading />;
}
return (
<div style={{ height: '10%' }}>
<Row justify="space-between" gutter={16} style={style}>
<Col span={12}>
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
{foodData.map((f, idx) => (
<FoodPowerup
key={idx}
food={f}
setHearts={setHearts}
currentHearts={currentHearts}
style={idx > 1 ? { marginLeft: 5 } : undefined}
/>
))}
</div>
</Col>
<Col span={12}>
{pokeballData.map((p, idx, arr) => (
<Powerup
key={idx}
icon={<TrademarkCircleOutlined />}
onClick={() => {
console.log('consume pokeball', p);
}}
style={idx !== arr.length - 1 ? { marginLeft: 5 } : undefined}
/>
))}
</Col>
</Row>
</div>
);
};
import { esh, pokemonWidth } from '../../data/constants';
import { PokemonOnCanvas } from '../../utils/pokemonFetcher';
type CanvasProps = {
pokemonsAlive: PokemonOnCanvas[];
};
type PokemonObjectProps = {
x: number;
y: number;
name: string;
sprite: string;
};
const Esh = () => (
<CanvasObject className="esh" {...esh} style={{ backgroundColor: 'red' }} />
);
const PokemonObject = ({ x, y, name, sprite }: PokemonObjectProps) => (
<CanvasObject
x={x}
y={y}
width={pokemonWidth}
style={{ backgroundImage: `url("${sprite}")` }}
label={name}
/>
);
type CanvasObjectProps = {
x: number;
y: number;
width: number;
label?: string;
className?: string;
style?: React.CSSProperties;
};
const CanvasObject = ({
x,
y,
width,
style = {},
className,
label
}: CanvasObjectProps) => (
<div
className={className}
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
className="canvas"
style={{ height: '85%', position: 'relative', overflow: 'hidden' }}
>
<Esh />
{pokemonsAlive.map(({ x, y, name, sprite }, idx) => (
<PokemonObject key={idx} x={x} y={y} name={name} sprite={sprite} />
))}
</div>
);
import { RestOutlined } from '@ant-design/icons';
import { maxHearts } from '../../data/constants';
import { Food } from '../../data/food';
import { Powerup } from './Powerup';
type FoodPowerupProps = {
setHearts: (hearts: number) => void;
currentHearts: number;
food: Food;
style?: React.CSSProperties;
};
export const FoodPowerup = ({
food,
setHearts,
currentHearts,
style
}: FoodPowerupProps) => {
const heal = (restores: number) => {
const healingEffect = currentHearts + restores;
setHearts(healingEffect > maxHearts ? maxHearts : healingEffect);
};
return (
<Powerup
icon={<RestOutlined />}
onClick={() => {
heal(food.restores);
}}
style={style}
/>
);
};
import { useEffect, useState } from 'react';
import { spawnPokemons } from '../../data/pokemons';
import { useGameInput } from '../../hooks/useGameInput';
import { getElementSize } from '../../utils/dom';
import { 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';
export type GamePhase = 'play' | 'prepare' | 'lost';
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;
};
const Game = ({
hearts,
setHearts,
pokemonsAlive,
setPokemonsAlive,
setScore,
score,
userData,
setGamePhase
}: GameProps) => {
const [input, setInput] = useGameInput();
const [colisionBorder, setColisionBorder] = useState<number>();