Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Task/refactor extract board #40

Merged
merged 1 commit into from Nov 7, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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;