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

Add game layout

parent 1caeac61
......@@ -5,4 +5,8 @@ html, body {
margin:0;
padding:0;
height:100%;
}
#root{
height:100%;
}
\ No newline at end of file
......@@ -19,7 +19,7 @@ const { Content } = Layout;
import { useMemo } from 'react';
import Encyclopedia from './pages/Encyclopedia';
import Game from './pages/Game';
import Game from './pages/Game/Game';
import Home from './pages/Home/Home';
import Leaderboard from './pages/Leaderboard/Leaderboard';
import Store from './pages/Store';
......@@ -32,7 +32,7 @@ const TopMenu = () => {
const { push } = useHistory();
return (
<Menu style={{ padding: '0 px' }} mode="horizontal">
<Menu style={{ padding: '0 px', height: '5%' }} mode="horizontal">
<Menu.Item key="home" icon={<HomeOutlined />}>
<Link to="/">
<Button type="text">Home</Button>
......@@ -82,7 +82,7 @@ const AppContent = () => {
const isLoggedIn = useMemo(() => currentUser !== undefined, [currentUser]);
return (
<Content style={{ padding: '0 50px' }}>
<Content style={{ padding: '0 50px', height: '95%' }}>
<Switch>
<Route path="/" exact component={Home} />
{isLoggedIn && (
......
export const maxHearts = 5;
export const pokemonSampleCount = 20;
......@@ -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 } 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
// data is saved result of getPokemons obtained from src/pokemonFetcher.ts
export const pokemons: Pokemon[] = [
{
......
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 useLoggedInUserData from '../../hooks/useLoggedInUserData';
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 = useLoggedInUserData();
const pokeballData = useMemo(
() => getUserPokeballs(userData?.pokeballIds),
[userData]
);
const foodData = useMemo(() => getUserFood(userData?.foodIds), [userData]);
if (pokeballData === null || foodData === null) {
return null;
}
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 { PokemonOnCanvas } from '../../utils/pokemonFetcher';
type CanvasProps = {
pokemonsAlive: PokemonOnCanvas[];
};
export const Canvas = ({ pokemonsAlive }: CanvasProps) => (
<div style={{ height: '85%' }}>Canvas</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, useMemo, useState } from 'react';
import { maxHearts } from '../../data/constants';
import { getPokemonsSample } from '../../data/pokemons';
import useLoggedInUserData from '../../hooks/useLoggedInUserData';
import usePageTitle from '../../hooks/usePageTitle';
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');
const userData = useLoggedInUserData();
const [hearts, setHearts] = useState(maxHearts);
const [score, setScore] = useState<number | undefined>();
const [pokemonsAlive, setPokemonsAlive] = useState<PokemonOnCanvas[]>(
getPokemonsSample()
);
const [gameStarted, setGameStarted] = useState(false);
const [input, setInput] = useState('');
const resetGame = () => {
setHearts(maxHearts);
setScore(userData?.actualScore);
setPokemonsAlive(getPokemonsSample());
};
const gameLost = useMemo(() => hearts < 0, [hearts]);
// set score after userData loads
useEffect(() => {
if (userData !== undefined) {
setScore(userData.actualScore);
}
}, [userData]);
// get new sample when pokemons are killed
useEffect(() => {
if (pokemonsAlive.length === 0) {
setPokemonsAlive(getPokemonsSample());
}
}, [pokemonsAlive]);
const tryPokemonCatch = (e: KeyboardEvent) => {
const newInput = input.concat(e.key);
const enterKeyCode = 13;
if (e.keyCode === enterKeyCode) {
setPokemonsAlive(pokemonsAlive.filter(p => p.name !== newInput));
setInput('');
} else {
setInput(newInput);
}
};
useEffect(() => {
document.addEventListener('keydown', tryPokemonCatch);
return () => {
document.removeEventListener('keydown', tryPokemonCatch);
};
}, []);
if (score === undefined) {
return null;
}
return (
<>
<TopBar hearts={hearts} score={score} />
<Canvas pokemonsAlive={pokemonsAlive} />
<BottomBar currentHearts={hearts} setHearts={setHearts} />
<RulesModal visible={!gameStarted} onOk={() => setGameStarted(true)} />
<GameLostModal visible={gameLost} onOk={resetGame} />
</>
);
};
export default Game;
import { Modal, Typography } from 'antd';
import { useHistory } from 'react-router-dom';
type GameLostModalProps = { visible: boolean; onOk: () => void };
export const GameLostModal = ({ visible, onOk }: GameLostModalProps) => {
const { push } = useHistory();
return (
<Modal
title={null}
visible={visible}
okText="Sure"
onOk={onOk}
cancelText="Nope, show me stats"
onCancel={() => push('/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';
type HeartsProps = {
hearts: number;
};
const Lives = ({ hearts }: HeartsProps) => (
<Rate
defaultValue={hearts}
count={maxHearts}
disabled
character={<HeartOutlined />}
/>
);
export default Lives;
import { TrademarkCircleOutlined } from '@ant-design/icons';
import { Pokeball } from '../../data/pokeballs';
import { Pokemon } from '../../utils/pokemonFetcher';
import { Powerup } from './Powerup';
type PokeballPowerupProps = {
pokeball: Pokeball;
setAlivePokemons: (alivePokemons: Pokemon[]) => void;
alivePokemons: Pokemon[];
style?: React.CSSProperties;
};
export const PokeballPowerup = ({
pokeball,
setAlivePokemons,
alivePokemons,
style
}: PokeballPowerupProps) => {
const catchPokemons = (catches: number) => {
setAlivePokemons(alivePokemons.slice(catches));
};
return (
<Powerup
icon={<TrademarkCircleOutlined />}
onClick={() => {
catchPokemons(pokeball.catches);
}}
style={style}
/>
);
};
import { Image } from 'antd';
type PokemonProps = {
sprite: string;
};
export const Pokemon = ({ sprite }: PokemonProps) => {
<Image width={200} src={sprite} />;
};
import { Button } from 'antd';
import { ReactNode } from 'react';
type PowerupProps = {
icon: ReactNode;
onClick: () => void;
style?: React.CSSProperties;
};
export const Powerup = ({ icon, onClick, style }: PowerupProps) => (
<Button
shape="circle"
size="large"
icon={icon}
onClick={onClick}
style={style}
/>
);
import { Modal, Typography } from 'antd';
import { useHistory } from 'react-router-dom';
type RulesModalProps = { visible: boolean; onOk: () => void };
export const RulesModal = ({ visible, onOk }: RulesModalProps) => {
const { push } = useHistory();
return (
<Modal
title="Game rules"
visible={visible}
okText="I Understand, let's go"
onOk={onOk}
cancelText="Hell no"
onCancel={() => push('/')}
>
<Typography>Rule 1</Typography>
<Typography>Rule 2</Typography>
<Typography>Rule 3</Typography>
</Modal>
);
};
import { MoneyCollectOutlined } from '@ant-design/icons';
import { Statistic } from 'antd';
type ScoreProps = {
score: number;
};
const Score = ({ score }: ScoreProps) => (
<Statistic value={score} prefix={<MoneyCollectOutlined />} />
);
export default Score;
import { Col, Row } from 'antd';
import Lives from './Lives';
import Score from './Score';
type TopBarProps = {
hearts: number;
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>
);
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