Unverified Commit 3f3374d8 authored by Michal Čaniga's avatar Michal Čaniga
Browse files

Add basic game layout

parent 07aa1272
......@@ -19,22 +19,23 @@ const { Content } = Layout;
import { useMemo } from 'react';
import Encyclopedia from './pages/Encyclopedia';
import Game from './pages/Game/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 (
......@@ -90,8 +91,8 @@ 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', height: '95%' }}>
......@@ -102,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>
......@@ -114,8 +115,12 @@ const App = () => (
<BrowserRouter>
<SelectedMenuKeyProvider>
<GameInputProvider>
<TopMenu />
<AppContent />
<UserProvider>
<AllUserDataProvider>
<TopMenu />
<AppContent />
</AllUserDataProvider>
</UserProvider>
</GameInputProvider>
</SelectedMenuKeyProvider>
</BrowserRouter>
......
......@@ -5,9 +5,8 @@ export const esh = {
y: 40,
width: 300
};
export const eshXColisionBorder = esh.x + esh.width;
export const pokemonWidth = 100;
export const pokemonSpawn = {
x: 120,
x: 101,
y: [40, 60]
};
......@@ -8,8 +8,17 @@ import { pokemonSampleCount, pokemonSpawn } from './constants';
export const getCatchedPokemons = (catchedPokemonIds: number[]): Pokemon[] =>
getDataByIds(pokemons, catchedPokemonIds);
export const spawnPokemons = (): PokemonOnCanvas[] => {
const [first, second] = sampleSize(pokemons, pokemonSampleCount);
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] },
......
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 { useEffect, useState } from 'react';
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>();
// 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();
};
}, [currentUser]);
return userData;
};
export default useLoggedInUserData;
......@@ -4,7 +4,7 @@ import { useMemo } from 'react';
import { getUserFood } from '../../data/food';
import { getUserPokeballs } from '../../data/pokeballs';
import useLoggedInUserData from '../../hooks/useLoggedInUserData';
import { useLoggedInUser } from '../../hooks/useLoggedInUser';
import { Loading } from '../../utils/Loading';
import { FoodPowerup } from './FoodPowerup';
......@@ -21,7 +21,7 @@ export const BottomBar = ({
setHearts,
currentHearts
}: BottomBarProps) => {
const userData = useLoggedInUserData();
const { userData } = useLoggedInUser();
const pokeballData = useMemo(
() => getUserPokeballs(userData?.pokeballIds),
[userData]
......
......@@ -9,15 +9,18 @@ type PokemonObjectProps = {
x: number;
y: number;
name: string;
sprite: string;
};
const Esh = () => <CanvasObject {...esh} style={{ backgroundColor: 'red' }} />;
const PokemonObject = ({ x, y, name }: PokemonObjectProps) => (
const Esh = () => (
<CanvasObject className="esh" {...esh} style={{ backgroundColor: 'red' }} />
);
const PokemonObject = ({ x, y, name, sprite }: PokemonObjectProps) => (
<CanvasObject
x={x}
y={y}
width={pokemonWidth}
style={{ backgroundColor: 'blue' }}
style={{ backgroundImage: `url("${sprite}")` }}
label={name}
/>
);
......@@ -27,6 +30,7 @@ type CanvasObjectProps = {
y: number;
width: number;
label?: string;
className?: string;
style?: React.CSSProperties;
};
......@@ -35,9 +39,11 @@ const CanvasObject = ({
y,
width,
style = {},
className,
label
}: CanvasObjectProps) => (
<div
className={className}
style={{
paddingTop: `${width}px`,
width: `${width}px`,
......@@ -67,10 +73,13 @@ const CanvasObject = ({
);
export const Canvas = ({ pokemonsAlive }: CanvasProps) => (
<div style={{ height: '85%', position: 'relative' }}>
<div
className="canvas"
style={{ height: '85%', position: 'relative', overflow: 'hidden' }}
>
<Esh />
{pokemonsAlive.map((p, idx) => (
<PokemonObject key={idx} x={p.x} y={p.y} name={p.name} />
{pokemonsAlive.map(({ x, y, name, sprite }, idx) => (
<PokemonObject key={idx} x={x} y={y} name={name} sprite={sprite} />
))}
</div>
);
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useEffect, useState } from 'react';
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 { 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 { GameLostModal } from './GameLostModal';
import { RulesModal } from './RulesModal';
import { TopBar } from './TopBar';
const Game = () => {
usePageTitle('Game');
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 userData = useLoggedInUserData();
const [hearts, setHearts] = useState(maxHearts);
const [score, setScore] = useState<number | undefined>();
const [pokemonsAlive, setPokemonsAlive] = useState<PokemonOnCanvas[]>(
spawnPokemons()
);
const [gameStarted, setGameStarted] = useState(false);
const Game = ({
hearts,
setHearts,
pokemonsAlive,
setPokemonsAlive,
setScore,
score,
userData,
setGamePhase
}: GameProps) => {
const [input, setInput] = useGameInput();
const [colisionBorder, setColisionBorder] = useState<number>();
// check if esh died
useEffect(() => {
if (hearts <= 0) {
setGamePhase('lost');
}
}, [hearts]);
const resetGame = useCallback(() => {
setHearts(maxHearts);
setScore(userData?.actualScore);
setPokemonsAlive(spawnPokemons());
}, [maxHearts, userData]);
// find out esh size
useEffect(() => {
const canvasHeight = getElementSize('.canvas')?.height;
const esh = getElementSize('.esh')?.height;
if (canvasHeight !== undefined && esh !== undefined) {
setColisionBorder((esh / canvasHeight) * 100);
}
const gameLost = useMemo(() => hearts < 0, [hearts]);
if (userData !== undefined) {
setScore(userData.actualScore);
}
});
// set score after userData loads
useEffect(() => {
......@@ -41,27 +64,39 @@ const Game = () => {
}
}, [userData]);
// get new sample when pokemons are killed
// spawn another pokemons after some interval
useEffect(() => {
if (pokemonsAlive.length === 0) {
setPokemonsAlive(spawnPokemons());
}
}, [pokemonsAlive]);
const id = setInterval(() => {
console.log('now');
setPokemonsAlive(prev => [...prev, ...spawnPokemons(prev)]);
}, 3000);
return () => clearInterval(id);
}, []);
// move pokemons
useEffect(() => {
const id = setInterval(() => {
const heartsLost = pokemonsAlive.filter(
p => p.x <= eshXColisionBorder
).length;
setHearts(hearts => (hearts - heartsLost > 0 ? hearts - heartsLost : 0));
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
}))
);
}, 500);
}, 20);
return () => clearInterval(id);
}, [pokemonsAlive]);
......@@ -77,8 +112,6 @@ const Game = () => {
}
};
console.log('input', input);
// Listen for user input, try to catch the pokemon
useEffect(() => {
document.addEventListener('keydown', tryToCatchPokemon);
......@@ -97,8 +130,6 @@ const Game = () => {
<TopBar hearts={hearts} score={score} />
<Canvas pokemonsAlive={pokemonsAlive} />
<BottomBar currentHearts={hearts} setHearts={setHearts} />
<RulesModal visible={!gameStarted} onOk={() => setGameStarted(true)} />
<GameLostModal visible={gameLost} onOk={resetGame} />
</>
);
};
......
import { useCallback, useState } from 'react';
import { maxHearts } from '../../data/constants';
import { spawnPokemons } from '../../data/pokemons';
import { useLoggedInUser } from '../../hooks/useLoggedInUser';
import usePageTitle from '../../hooks/usePageTitle';
import { PokemonOnCanvas } from '../../utils/pokemonFetcher';
import Game, { GamePhase } 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>();
const [pokemonsAlive, setPokemonsAlive] = useState<PokemonOnCanvas[]>(
spawnPokemons([])
);
const [gamePhase, setGamePhase] = useState<GamePhase>('prepare');
const resetGame = useCallback(() => {
setHearts(maxHearts);
setScore(userData?.actualScore);
setPokemonsAlive(spawnPokemons([]));
setGamePhase('play');
}, [maxHearts, userData]);
return (
<>
{gamePhase === 'play' && (
<Game
hearts={hearts}
setHearts={setHearts}
score={score}
setScore={setScore}
pokemonsAlive={pokemonsAlive}
setPokemonsAlive={setPokemonsAlive}
setGamePhase={setGamePhase}
userData={userData}
/>
)}
{gamePhase === 'prepare' && (
<RulesModal visible onOk={() => setGamePhase('play')} />
)}
{gamePhase === 'lost' && <GameLostModal visible onOk={resetGame} />}
</>
);
};
export default GamePage;
import { Col, Row, Typography } from 'antd';
import { Col, Row } from 'antd';
import { useGameInput } from '../../hooks/useGameInput';
......@@ -13,7 +13,7 @@ type TopBarProps = {
export const TopBar = ({ hearts, score }: TopBarProps) => {
const [input] = useGameInput();
return (
<Row justify="space-between">
<Row justify="space-between" style={{ height: '5%' }}>
<Col span={8}>
<Score score={score} />
</Col>
......
......@@ -99,6 +99,10 @@ export const AuthModal = ({
[setAuthModalType, setIsAuthModalVisible, type]
);
if (!isVisible) {
return null;
}
if (type === undefined || title === undefined) {
return <Loading />;
}
......
import { Button, Typography } from 'antd';
import { useMemo, useState } from 'react';
import useLoggedInUser from '../../hooks/useLoggedInUser';
import { useLoggedInUser } from '../../hooks/useLoggedInUser';
import usePageTitle from '../../hooks/usePageTitle';
import { AuthModal, AuthModalType, getAuthModalTitle } from './AuthModal';
......@@ -9,9 +9,8 @@ import { HomeCarousel } from './HomeCarousel';
const Home = () => {
usePageTitle('Home');
const currentUser = useLoggedInUser();
const isLoggedIn = useMemo(() => currentUser !== undefined, [currentUser]);
const { user } = useLoggedInUser();
const isLoggedIn = useMemo(() => user !== undefined, [user]);
const [isAuthModalVisible, setIsAuthModalVisible] = useState(false);
const [authModalType, setAuthModalType] = useState<AuthModalType>();
......
import { Spin, Table } from 'antd';
import { Table } from 'antd';
import { SortOrder } from 'antd/lib/table/interface';
import { User } from 'firebase/auth';
import { useMemo, useState } from 'react';
import useLoggedInUser from '../../hooks/useLoggedInUser';
import { useLoggedInUser } from '../../hooks/useLoggedInUser';
import usePageTitle from '../../hooks/usePageTitle';
import { UserData } from '../../utils/firebase';
import './Leaderboard.css';
import { makeStringSorter, makeNumberSorter } from '../../utils/sorters';
import useAllUserData from '../../hooks/useAllUserData';
import { useAllUserData } from '../../hooks/useAllUserData';
import { Loading } from '../../utils/Loading';
import {
......@@ -70,24 +70,24 @@ const computeColumns = (page: number, pageSize: number, currentUser?: User) =>
const Leaderboard = () => {
usePageTitle('Leaderboard');
const pageSize = 10;
const currentUser = useLoggedInUser();
const { user } = useLoggedInUser();
const allUserData = useAllUserData();
const [page, setPage] = useState(1);
const columns = useMemo(
() => computeColumns(page, pageSize, currentUser),
[page, pageSize, currentUser]
() => computeColumns(page, pageSize, user),
[page, pageSize, user]
);
const leaderboardData = useMemo(
() => makeLeaderboardData(allUserData),
[allUserData]
);
const currentUserData = useMemo(() => {
if (currentUser === undefined || allUserData === undefined) {
if (user === undefined || allUserData === undefined) {
return null;
}
return allUserData.filter(d => d.userId === currentUser.uid)[0];
}, [page, pageSize, currentUser, allUserData]);
return allUserData.filter(d => d.userId === user.uid)[0];
}, [page, pageSize, user, allUserData]);
if (
columns === null ||
......
......@@ -23,7 +23,7 @@ export const makeLeaderboardCellRenderer =
export const makeIndexRenderer =
(page: number, pageSize: number, currentUser: User) =>