Unverified Commit 07aa1272 authored by Michal Čaniga's avatar Michal Čaniga
Browse files

Add game layout

parent aa46a8a3
......@@ -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>
);
......
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]
};
......@@ -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[] = [
......
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);
......@@ -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>();
......
......@@ -20,7 +20,7 @@ const useLoggedInUserData = () => {
return () => {
unsubscribe();
};
}, []);
}, [currentUser]);
return userData;
};
......
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);
......@@ -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 (
......
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>
);
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[]>(
getPokemonsSample()
spawnPokemons()
);
const [gameStarted, setGameStarted] = useState(false);
const [input, setInput] = useState('');
const [input, setInput] = useGameInput();
const resetGame = () => {
const resetGame = useCallback(() => {
setHearts(maxHearts);
setScore(userData?.actualScore);
setPokemonsAlive(getPokemonsSample());
};
setPokemonsAlive(spawnPokemons());
}, [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(getPokemonsSample());
setPokemonsAlive(spawnPokemons());
}
}, [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', tryPokemonCatch);
document.addEventListener('keydown', tryToCatchPokemon);
return () => {
document.removeEventListener('keydown', tryPokemonCatch);
document.removeEventListener('keydown', tryToCatchPokemon);
};
}, []);
}, [input, setPokemonsAlive, setInput]);
if (score === undefined) {
return null;
return <Loading />;
}
return (
......
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>
......
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;
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>
......
import { MoneyCollectOutlined } from '@ant-design/icons';
import { DollarOutlined } 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;
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>
);
};
......@@ -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 (
......
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 (
......
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>
);
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment