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 {
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>
);
......
export const maxHearts = 5;
export const catchReward = 10;
export const pokemonSampleCount = 2;
export const esh = {
x: 0,
......
......@@ -20,6 +20,12 @@ export const food: Food[] = [
name: 'Chicken Wings',
price: 471,
restores: 2
},
{
id: 3,
name: 'Chicken Nuggets',
price: 682,
restores: 3
}
];
......
......@@ -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();
......
......@@ -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 () => {
......
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 { 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 { PokeballPowerup } from './PokeballPowerup';
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
<PokeballPowerup
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}
/>
))}
......
......@@ -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
......
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>
);
};
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}
/>
</>
);
};
......
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');
......
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 { PokemonOnCanvas } from '../../utils/pokemonFetcher';
import { GamePhase } 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<