Unverified Commit 410071e2 authored by Michal Čaniga's avatar Michal Čaniga
Browse files

Finish game mechanics, polish the website

parent 1fee183d
...@@ -32,6 +32,8 @@ import { ...@@ -32,6 +32,8 @@ import {
import { GameInputProvider } from './hooks/useGameInput'; import { GameInputProvider } from './hooks/useGameInput';
import { AllUserDataProvider } from './hooks/useAllUserData'; import { AllUserDataProvider } from './hooks/useAllUserData';
import GamePage from './pages/Game/GamePage'; import GamePage from './pages/Game/GamePage';
import { NotFoundPage } from './pages/NotFoundPage';
import { Forbidden } from './pages/Forbidden';
const TopMenu = () => { const TopMenu = () => {
const { user } = useLoggedInUser(); const { user } = useLoggedInUser();
...@@ -98,14 +100,23 @@ const AppContent = () => { ...@@ -98,14 +100,23 @@ const AppContent = () => {
<Content style={{ padding: '0 50px', height: '95%' }}> <Content style={{ padding: '0 50px', height: '95%' }}>
<Switch> <Switch>
<Route path="/" exact component={Home} /> <Route path="/" exact component={Home} />
{isLoggedIn && ( <Route path="/store" exact component={isLoggedIn ? Store : Forbidden} />
<> <Route
<Route path="/store" exact component={Store} /> path="/encyclopedia"
<Route path="/encyclopedia" exact component={Encyclopedia} /> exact
<Route path="/leaderboard" exact component={Leaderboard} /> component={isLoggedIn ? Encyclopedia : Forbidden}
<Route path="/game" exact component={GamePage} /> />
</> <Route
)} path="/leaderboard"
exact
component={isLoggedIn ? Leaderboard : Forbidden}
/>
<Route
path="/game"
exact
component={isLoggedIn ? GamePage : Forbidden}
/>
<Route path="*" component={NotFoundPage} />
</Switch> </Switch>
</Content> </Content>
); );
......
export const maxHearts = 5; export const maxHearts = 5;
export const catchReward = 10;
export const pokemonSampleCount = 2; export const pokemonSampleCount = 2;
export const esh = { export const esh = {
x: 0, x: 0,
......
...@@ -20,6 +20,12 @@ export const food: Food[] = [ ...@@ -20,6 +20,12 @@ export const food: Food[] = [
name: 'Chicken Wings', name: 'Chicken Wings',
price: 471, price: 471,
restores: 2 restores: 2
},
{
id: 3,
name: 'Chicken Nuggets',
price: 682,
restores: 3
} }
]; ];
......
...@@ -10,7 +10,7 @@ export const AllUserDataProvider: FC = ({ children }) => { ...@@ -10,7 +10,7 @@ export const AllUserDataProvider: FC = ({ children }) => {
useEffect(() => { useEffect(() => {
const unsubscribe = onSnapshot(userDataCollection, snapshot => { const unsubscribe = onSnapshot(userDataCollection, snapshot => {
setAllUserData(snapshot.docs.map(doc => doc.data())); setAllUserData(snapshot.docs.map(doc => ({ ...doc.data(), id: doc.id })));
}); });
return () => { return () => {
unsubscribe(); unsubscribe();
......
...@@ -24,7 +24,10 @@ export const UserProvider: FC = ({ children }) => { ...@@ -24,7 +24,10 @@ export const UserProvider: FC = ({ children }) => {
if (user === undefined) { if (user === undefined) {
return; 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]); setUserData(allUserData.filter(ud => ud.userId === user.uid)[0]);
}); });
return () => { return () => {
......
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}
/>
</>
}
/>
);
};
import { TrademarkCircleOutlined } from '@ant-design/icons';
import { Row, Col } from 'antd'; import { Row, Col } from 'antd';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { getUserFood } from '../../data/food'; import { getUserFood } from '../../data/food';
import { getUserPokeballs } from '../../data/pokeballs'; import { getUserPokeballs } from '../../data/pokeballs';
import { useLoggedInUser } from '../../hooks/useLoggedInUser'; import { useLoggedInUser } from '../../hooks/useLoggedInUser';
import {
SetPokemonsAlive,
SetScore,
SetUserData
} from '../../utils/commonTypes';
import { UserData } from '../../utils/firebase';
import { Loading } from '../../utils/Loading'; import { Loading } from '../../utils/Loading';
import { PokemonOnCanvas } from '../../utils/pokemonFetcher';
import { FoodPowerup } from './FoodPowerup'; import { FoodPowerup } from './FoodPowerup';
import { Powerup } from './Powerup'; import { PokeballPowerup } from './PokeballPowerup';
type BottomBarProps = { type BottomBarProps = {
style?: React.CSSProperties; style?: React.CSSProperties;
setHearts: (hearts: number) => void; setHearts: (hearts: number) => void;
currentHearts: number; currentHearts: number;
newUserData: UserData;
setScore: SetScore;
setNewUserData: SetUserData;
alivePokemons: PokemonOnCanvas[];
setAlivePokemons: SetPokemonsAlive;
}; };
export const BottomBar = ({ export const BottomBar = ({
style, style,
setHearts, setHearts,
currentHearts currentHearts,
newUserData,
setScore,
setNewUserData,
alivePokemons,
setAlivePokemons
}: BottomBarProps) => { }: BottomBarProps) => {
const { userData } = useLoggedInUser();
const pokeballData = useMemo( const pokeballData = useMemo(
() => getUserPokeballs(userData?.pokeballIds), () => getUserPokeballs(newUserData.pokeballIds),
[userData] [newUserData]
);
const foodData = useMemo(
() => getUserFood(newUserData.foodIds),
[newUserData]
); );
const foodData = useMemo(() => getUserFood(userData?.foodIds), [userData]);
if (pokeballData === null || foodData === null) { if (pokeballData === null || foodData === null) {
return <Loading />; return <Loading />;
...@@ -43,6 +61,7 @@ export const BottomBar = ({ ...@@ -43,6 +61,7 @@ export const BottomBar = ({
food={f} food={f}
setHearts={setHearts} setHearts={setHearts}
currentHearts={currentHearts} currentHearts={currentHearts}
setNewUserData={setNewUserData}
style={idx > 1 ? { marginLeft: 5 } : undefined} style={idx > 1 ? { marginLeft: 5 } : undefined}
/> />
))} ))}
...@@ -50,12 +69,13 @@ export const BottomBar = ({ ...@@ -50,12 +69,13 @@ export const BottomBar = ({
</Col> </Col>
<Col span={12}> <Col span={12}>
{pokeballData.map((p, idx, arr) => ( {pokeballData.map((p, idx, arr) => (
<Powerup <PokeballPowerup
key={idx} key={idx}
icon={<TrademarkCircleOutlined />} alivePokemons={alivePokemons}
onClick={() => { pokeball={p}
console.log('consume pokeball', p); setAlivePokemons={setAlivePokemons}
}} setNewUserData={setNewUserData}
setScore={setScore}
style={idx !== arr.length - 1 ? { marginLeft: 5 } : undefined} style={idx !== arr.length - 1 ? { marginLeft: 5 } : undefined}
/> />
))} ))}
......
...@@ -13,7 +13,11 @@ type PokemonObjectProps = { ...@@ -13,7 +13,11 @@ type PokemonObjectProps = {
}; };
const Esh = () => ( 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) => ( const PokemonObject = ({ x, y, name, sprite }: PokemonObjectProps) => (
<CanvasObject <CanvasObject
......
import { RestOutlined } from '@ant-design/icons'; import { RestOutlined } from '@ant-design/icons';
import { Badge } from 'antd';
import { maxHearts } from '../../data/constants'; import { maxHearts } from '../../data/constants';
import { Food } from '../../data/food'; import { Food } from '../../data/food';
import { SetUserData } from '../../utils/commonTypes';
import { Powerup } from './Powerup'; import { Powerup } from './Powerup';
...@@ -10,13 +12,15 @@ type FoodPowerupProps = { ...@@ -10,13 +12,15 @@ type FoodPowerupProps = {
currentHearts: number; currentHearts: number;
food: Food; food: Food;
style?: React.CSSProperties; style?: React.CSSProperties;
setNewUserData: SetUserData;
}; };
export const FoodPowerup = ({ export const FoodPowerup = ({
food, food,
setHearts, setHearts,
currentHearts, currentHearts,
style style,
setNewUserData
}: FoodPowerupProps) => { }: FoodPowerupProps) => {
const heal = (restores: number) => { const heal = (restores: number) => {
const healingEffect = currentHearts + restores; const healingEffect = currentHearts + restores;
...@@ -24,12 +28,22 @@ export const FoodPowerup = ({ ...@@ -24,12 +28,22 @@ export const FoodPowerup = ({
}; };
return ( return (
<Powerup <Badge count={food.restores}>
icon={<RestOutlined />} <Powerup
onClick={() => { icon={<RestOutlined />}
heal(food.restores); onClick={() => {
}} heal(food.restores);
style={style} setNewUserData(prev =>
/> prev !== undefined
? {
...prev,
foodIds: prev.foodIds.filter(id => id !== food.id)
}
: undefined
);
}}
style={style}
/>
</Badge>
); );
}; };
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 { spawnPokemons } from '../../data/pokemons';
import { useGameInput } from '../../hooks/useGameInput'; import { useGameInput } from '../../hooks/useGameInput';
import { useLoggedInUser } from '../../hooks/useLoggedInUser';
import {
SetGamePhase,
SetPokemonsAlive,
SetScore,
SetUserData
} from '../../utils/commonTypes';
import { getElementSize } from '../../utils/dom'; import { getElementSize } from '../../utils/dom';
import { UserData } from '../../utils/firebase'; import { updateUserDataInFirebase, UserData } from '../../utils/firebase';
import { Loading } from '../../utils/Loading'; import { Loading } from '../../utils/Loading';
import { PokemonOnCanvas } from '../../utils/pokemonFetcher'; import { PokemonOnCanvas } from '../../utils/pokemonFetcher';
import { BottomBar } from './BottomBar'; import { BottomBar } from './BottomBar';
import { Canvas } from './Canvas'; 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 = { type GameProps = {
hearts: number; setGamePhase: SetGamePhase;
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 = ({ const onPokemonCatch = (
hearts, setPokemonsAlive: SetPokemonsAlive,
setHearts, setScore: SetScore,
pokemonsAlive, setNewUserData: SetUserData,
setPokemonsAlive, setInput: SetInput,
setScore, input: string,
score, catchedIds: number[]
userData, ) => {
setGamePhase setPokemonsAlive(prevPokemons => prevPokemons.filter(p => p.name !== input));
}: GameProps) => { 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 [input, setInput] = useGameInput();
const [colisionBorder, setColisionBorder] = useState<number>(); const [colisionBorder, setColisionBorder] = useState<number>();
const [newUserData, setNewUserData] = useState<UserData>();
// check if esh died // check if esh died
useEffect(() => { useEffect(() => {
if (hearts <= 0) { if (hearts <= 0) {
setGamePhase('lost'); onEshDeath(setGamePhase, userData!, newUserData!, score!, seconds);
} }
}, [hearts]); }, [hearts, setGamePhase, userData]);
// find out esh size // find out esh width
useEffect(() => { useEffect(() => {
const canvasHeight = getElementSize('.canvas')?.height; const canvasWidth = getElementSize('.canvas')?.width;
const esh = getElementSize('.esh')?.height; const esh = getElementSize('.esh')?.width;
if (canvasHeight !== undefined && esh !== undefined) { if (canvasWidth !== undefined && esh !== undefined) {
setColisionBorder((esh / canvasHeight) * 100); setColisionBorder((esh / canvasWidth) * 100);
}
if (userData !== undefined) {
setScore(userData.actualScore);
} }
}); });
// 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(() => { useEffect(() => {
if (userData !== undefined) { if (userData !== undefined) {
setScore(userData.actualScore); setScore(userData.actualScore);
setNewUserData(cloneDeep(userData));
} }
}, [userData]); }, [userData, setScore, setNewUserData]);
// spawn another pokemons after some interval // spawn another pokemons after some interval
useEffect(() => { useEffect(() => {
const id = setInterval(() => { const id = setInterval(() => {
console.log('now'); spawnNextWawe(setPokemonsAlive);
setPokemonsAlive(prev => [...prev, ...spawnPokemons(prev)]);
}, 3000); }, 3000);
return () => clearInterval(id); return () => clearInterval(id);
}, []); }, []);
//count seconds
useEffect(() => {
const id = setInterval(() => {
setSeconds(prev => prev + 1);
}, 1000);
return () => clearInterval(id);
}, []);
// move pokemons // move pokemons
useEffect(() => { useEffect(() => {
const id = setInterval(() => { const id = setInterval(() => {
if (colisionBorder !== undefined) { movePokemons(pokemonsAlive, setPokemonsAlive, setHearts, colisionBorder);
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
}))
);
}, 20); }, 20);
return () => clearInterval(id); return () => clearInterval(id);