Skip to content

Commit

Permalink
Extract a board component from App.tsx
Browse files Browse the repository at this point in the history
  • Loading branch information
sburba committed Nov 7, 2019
1 parent e7c5481 commit dede73b
Show file tree
Hide file tree
Showing 2 changed files with 183 additions and 147 deletions.
166 changes: 19 additions & 147 deletions web/src/ui/App.tsx
@@ -1,52 +1,19 @@
import React, { MouseEvent, useEffect, useState } from "react";
import CardToken from "./token/CardToken";
import React, { useEffect, useState } from "react";
import { List } from "immutable";
import { makeStyles } from "@material-ui/core";
import { GRID_SIZE_PX } from "../config";
import CardTokenSheet from "./CardTokenSheet";
import { TokenState, TokenStateClient } from "../network/TokenStateClient";
import uuid from "uuid";
import { Icon, ICONS, ICONS_BY_ID, IconType, WALL_ICON } from "./icons";
import FloorToken from "./token/FloorToken";
import UnreachableCaseError from "../util/UnreachableCaseError";
import { Icon, ICONS, IconType, WALL_ICON } from "./icons";
import FloorTokenSheet from "./FloorTokenSheet";
import Board from "./Board";

let BACKGROUND_COLOR = "#F5F5DC";
let GRID_COLOR = "#947C65";

const dragSnapToGrid = (x: number) =>
Math.round(x / GRID_SIZE_PX) * GRID_SIZE_PX;
const clickSnapToGrid = (x: number) =>
Math.floor(x / GRID_SIZE_PX) * GRID_SIZE_PX;

const useStyles = makeStyles(theme => ({
map: {
backgroundColor: BACKGROUND_COLOR,
backgroundImage: `repeating-linear-gradient(
0deg,
transparent,
transparent ${GRID_SIZE_PX - 1}px,
${GRID_COLOR} ${GRID_SIZE_PX - 1}px,
${GRID_COLOR} ${GRID_SIZE_PX}px
),
repeating-linear-gradient(
-90deg,
transparent,
transparent ${GRID_SIZE_PX - 1}px,
${GRID_COLOR} ${GRID_SIZE_PX - 1}px,
${GRID_COLOR} ${GRID_SIZE_PX}px
)`,
backgroundSize: `${GRID_SIZE_PX}px ${GRID_SIZE_PX}px`,
height: "100%",
width: "100%",
zIndex: 0,
position: "absolute",
// All the tokens inside the map have to be position absolute so that the
// drag offset calculations work properly
"& div": {
position: "absolute"
}
},
tokenSheet: {
display: "flex",
flexDirection: "column",
Expand All @@ -67,12 +34,6 @@ const useStyles = makeStyles(theme => ({
}
}));

enum MouseType {
drawing_walls,
deleting,
none
}

const TOKEN_SHEET_ICONS = ICONS.filter(icon => icon.type === IconType.token);
const FLOOR_SHEET_ICONS = ICONS.filter(
icon => icon.type === IconType.floor || icon.type === IconType.wall
Expand All @@ -81,7 +42,6 @@ const FLOOR_SHEET_ICONS = ICONS.filter(
const App = () => {
const classes = useStyles();

const [mouseType, setMouseType] = useState<MouseType>(MouseType.none);
const [activeFloor, setActiveFloor] = useState(WALL_ICON);
const [tokens, setTokens] = useState(List.of<TokenState>());
const [client, setClient] = useState<TokenStateClient>();
Expand Down Expand Up @@ -133,120 +93,32 @@ const App = () => {
setTokens(tokens.push(token));
};

const tokenIcons = tokens.map((token, i) => {
const icon = ICONS_BY_ID.get(token.iconId, WALL_ICON);
const pos = { x: token.x, y: token.y, z: token.z };
const onDropped = (x: number, y: number) => {
const newToken = {
id: token.id,
x: dragSnapToGrid(x),
y: dragSnapToGrid(y),
z: token.z,
iconId: token.iconId
};
if (client) {
client.queueUpdate(newToken);
}
setTokens(tokens.set(i, newToken));
};

switch (icon.type) {
case IconType.floor:
case IconType.wall:
return <FloorToken key={token.id} icon={icon} pos={pos} />;
case IconType.token:
return (
<CardToken
key={token.id}
icon={icon}
pos={pos}
onDropped={onDropped}
/>
);
default:
throw new UnreachableCaseError(icon.type);
}
});

const onMapMouseDown = (e: MouseEvent) => {
if (e.button === 0) {
setMouseType(MouseType.drawing_walls);
placeFloorAt(activeFloor, e.clientX, e.clientY);
} else if (e.button === 2) {
setMouseType(MouseType.deleting);
deleteAt(e.clientX, e.clientY);
}
};

const onMapMouseUp = () => setMouseType(MouseType.none);

const placeFloorAt = (icon: Icon, x: number, y: number) => {
const gridX = clickSnapToGrid(x);
const gridY = clickSnapToGrid(y);

if (getTokenAt(gridX, gridY)) {
return;
}

const token = {
id: uuid(),
x: clickSnapToGrid(x),
y: clickSnapToGrid(y),
z: 0,
iconId: icon.id
};
const deleteToken = (tokenId: string) => {
setTokens(tokens.delete(tokens.findIndex(token => token.id === tokenId)));
if (client) {
client.queueCreate(token);
}
setTokens(tokens.push(token));
};

const deleteAt = (x: number, y: number) => {
const gridX = clickSnapToGrid(x);
const gridY = clickSnapToGrid(y);
const token = tokens
.filter(token => token.x === gridX && token.y === gridY)
.maxBy(token => token.z);

if (client && token) {
client.queueDelete(token.id);
}
if (token) {
setTokens(tokens.delete(tokens.indexOf(token)));
client.queueDelete(tokenId);
}
};

const getTokenAt = (x: number, y: number): TokenState | null => {
const gridX = clickSnapToGrid(x);
const gridY = clickSnapToGrid(y);
for (const token of tokens) {
if (token.x === gridX && token.y === gridY) {
return token;
}
}

return null;
const createToken = (token: TokenState) => {
setTokens(tokens.push(token));
};

const onMapMouseMoving = (e: MouseEvent) => {
if (mouseType === MouseType.drawing_walls) {
placeFloorAt(activeFloor, e.clientX, e.clientY);
} else if (mouseType === MouseType.deleting) {
deleteAt(e.clientX, e.clientY);
}
const updateToken = (newToken: TokenState) => {
setTokens(
tokens.set(tokens.findIndex(token => token.id === newToken.id), newToken)
);
};

return (
<div>
<div
className={classes.map}
onMouseUp={onMapMouseUp}
onMouseDown={onMapMouseDown}
onMouseMove={onMapMouseMoving}
onContextMenu={e => e.preventDefault()}
>
{tokenIcons}
</div>
<Board
tokens={tokens}
onTokenCreated={createToken}
onTokenDeleted={deleteToken}
onTokenMoved={updateToken}
activeFloor={activeFloor}
/>
<div className={classes.tokenSheet}>
<CardTokenSheet
tokenTypes={TOKEN_SHEET_ICONS}
Expand Down
164 changes: 164 additions & 0 deletions web/src/ui/Board.tsx
@@ -0,0 +1,164 @@
import React, { MouseEvent, useState } from "react";
import { makeStyles } from "@material-ui/core";
import { GRID_SIZE_PX } from "../config";
import { Icon, ICONS_BY_ID, IconType, WALL_ICON } from "./icons";
import uuid from "uuid";
import { TokenState } from "../network/TokenStateClient";
import { List } from "immutable";
import FloorToken from "./token/FloorToken";
import CardToken from "./token/CardToken";
import UnreachableCaseError from "../util/UnreachableCaseError";

let BACKGROUND_COLOR = "#F5F5DC";
let GRID_COLOR = "#947C65";

const useStyles = makeStyles({
board: {
backgroundColor: BACKGROUND_COLOR,
backgroundImage: `repeating-linear-gradient(
0deg,
transparent,
transparent ${GRID_SIZE_PX - 1}px,
${GRID_COLOR} ${GRID_SIZE_PX - 1}px,
${GRID_COLOR} ${GRID_SIZE_PX}px
),
repeating-linear-gradient(
-90deg,
transparent,
transparent ${GRID_SIZE_PX - 1}px,
${GRID_COLOR} ${GRID_SIZE_PX - 1}px,
${GRID_COLOR} ${GRID_SIZE_PX}px
)`,
backgroundSize: `${GRID_SIZE_PX}px ${GRID_SIZE_PX}px`,
height: "100%",
width: "100%",
zIndex: 0,
position: "absolute",
// All the tokens inside the map have to be position absolute so that the
// drag offset calculations work properly
"& div": {
position: "absolute"
}
}
});

enum MouseType {
drawing_walls = "walls",
deleting = "deleting",
none = "none"
}

const dragSnapToGrid = (x: number) =>
Math.round(x / GRID_SIZE_PX) * GRID_SIZE_PX;
const clickSnapToGrid = (x: number) =>
Math.floor(x / GRID_SIZE_PX) * GRID_SIZE_PX;

interface Props {
tokens: List<TokenState>;
activeFloor: Icon;
onTokenCreated: (token: TokenState) => void;
onTokenDeleted: (tokenId: string) => void;
onTokenMoved: (token: TokenState) => void;
}

const Board = (props: Props) => {
const classes = useStyles();
const [mouseType, setMouseType] = useState<MouseType>(MouseType.none);

const onMouseMove = (e: MouseEvent) => {
if (mouseType === MouseType.drawing_walls) {
placeFloorAt(props.activeFloor, e.clientX, e.clientY);
} else if (mouseType === MouseType.deleting) {
deleteAt(e.clientX, e.clientY);
}
};

const onMouseDown = (e: MouseEvent) => {
if (e.button === 0) {
setMouseType(MouseType.drawing_walls);
placeFloorAt(props.activeFloor, e.clientX, e.clientY);
} else if (e.button === 2) {
setMouseType(MouseType.deleting);
deleteAt(e.clientX, e.clientY);
}
};

const deleteAt = (x: number, y: number) => {
const token = getTokenAt(x, y);
if (token) {
props.onTokenDeleted(token.id);
}
};

const getTokenAt = (x: number, y: number) => {
const gridX = clickSnapToGrid(x);
const gridY = clickSnapToGrid(y);

return props.tokens
.filter(token => token.x === gridX && token.y === gridY)
.maxBy(token => token.z);
};

const placeFloorAt = (icon: Icon, x: number, y: number) => {
if (getTokenAt(x, y)) {
return;
}

props.onTokenCreated({
id: uuid(),
x: clickSnapToGrid(x),
y: clickSnapToGrid(y),
z: 0,
iconId: icon.id
});
};

const onMouseUp = () => setMouseType(MouseType.none);

const tokenIcons = props.tokens.map(token => {
const icon = ICONS_BY_ID.get(token.iconId, WALL_ICON);
const pos = { x: token.x, y: token.y, z: token.z };

const onDropped = (x: number, y: number) => {
const newToken = {
id: token.id,
x: dragSnapToGrid(x),
y: dragSnapToGrid(y),
z: token.z,
iconId: token.iconId
};
props.onTokenMoved(newToken);
};

switch (icon.type) {
case IconType.floor:
case IconType.wall:
return <FloorToken key={token.id} icon={icon} pos={pos} />;
case IconType.token:
return (
<CardToken
key={token.id}
icon={icon}
pos={pos}
onDropped={onDropped}
/>
);
default:
throw new UnreachableCaseError(icon.type);
}
});

return (
<div
className={classes.board}
onMouseUp={onMouseUp}
onMouseDown={onMouseDown}
onMouseMove={onMouseMove}
onContextMenu={e => e.preventDefault()}
>
{tokenIcons}
</div>
);
};

export default Board;

0 comments on commit dede73b

Please sign in to comment.