2021-06-10 11:16:47 +10:00
|
|
|
import React, { useState, useRef } from "react";
|
2021-06-12 18:40:02 +10:00
|
|
|
import { Image as KonvaImage, Group } from "react-konva";
|
2020-05-25 10:37:28 +10:00
|
|
|
import { useSpring, animated } from "react-spring/konva";
|
2020-05-21 16:46:50 +10:00
|
|
|
import useImage from "use-image";
|
2020-04-13 00:24:03 +10:00
|
|
|
|
2021-02-04 15:06:34 +11:00
|
|
|
import usePrevious from "../../hooks/usePrevious";
|
2021-02-06 19:29:24 +11:00
|
|
|
import useGridSnapping from "../../hooks/useGridSnapping";
|
2020-04-24 15:50:05 +10:00
|
|
|
|
2021-06-24 16:14:20 +10:00
|
|
|
import { useUserId } from "../../contexts/UserIdContext";
|
2021-03-12 11:02:58 +11:00
|
|
|
import {
|
|
|
|
|
useSetPreventMapInteraction,
|
|
|
|
|
useMapWidth,
|
|
|
|
|
useMapHeight,
|
|
|
|
|
} from "../../contexts/MapInteractionContext";
|
|
|
|
|
import { useGridCellPixelSize } from "../../contexts/GridContext";
|
2021-04-22 16:53:35 +10:00
|
|
|
import { useDataURL } from "../../contexts/AssetsContext";
|
2020-05-18 19:21:29 +10:00
|
|
|
|
2020-05-21 20:57:52 +10:00
|
|
|
import TokenStatus from "../token/TokenStatus";
|
|
|
|
|
import TokenLabel from "../token/TokenLabel";
|
2021-06-12 18:40:02 +10:00
|
|
|
import TokenOutline from "../token/TokenOutline";
|
2021-04-24 18:21:49 +10:00
|
|
|
|
2021-06-12 20:53:40 +10:00
|
|
|
import { Intersection, getScaledOutline } from "../../helpers/token";
|
|
|
|
|
|
2021-04-24 18:21:49 +10:00
|
|
|
import { tokenSources } from "../../tokens";
|
2020-04-13 00:24:03 +10:00
|
|
|
|
2020-05-21 16:46:50 +10:00
|
|
|
function MapToken({
|
|
|
|
|
tokenState,
|
|
|
|
|
onTokenStateChange,
|
|
|
|
|
onTokenMenuOpen,
|
2020-05-21 22:57:44 +10:00
|
|
|
onTokenDragStart,
|
|
|
|
|
onTokenDragEnd,
|
2020-05-22 15:11:18 +10:00
|
|
|
draggable,
|
2020-07-31 14:50:01 +10:00
|
|
|
fadeOnHover,
|
2020-08-07 12:55:16 +10:00
|
|
|
map,
|
2020-05-21 16:46:50 +10:00
|
|
|
}) {
|
2021-06-24 16:14:20 +10:00
|
|
|
const userId = useUserId();
|
2021-03-12 11:02:58 +11:00
|
|
|
|
|
|
|
|
const mapWidth = useMapWidth();
|
|
|
|
|
const mapHeight = useMapHeight();
|
|
|
|
|
const setPreventMapInteraction = useSetPreventMapInteraction();
|
|
|
|
|
|
|
|
|
|
const gridCellPixelSize = useGridCellPixelSize();
|
2020-05-21 16:46:50 +10:00
|
|
|
|
2021-04-24 18:21:49 +10:00
|
|
|
const tokenURL = useDataURL(tokenState, tokenSources);
|
2021-06-10 11:16:47 +10:00
|
|
|
const [tokenImage] = useImage(tokenURL);
|
2020-05-21 16:46:50 +10:00
|
|
|
|
2021-04-24 18:21:49 +10:00
|
|
|
const tokenAspectRatio = tokenState.width / tokenState.height;
|
2020-05-21 16:46:50 +10:00
|
|
|
|
2021-02-09 14:13:08 +11:00
|
|
|
const snapPositionToGrid = useGridSnapping();
|
2021-02-06 19:29:24 +11:00
|
|
|
|
2020-05-22 20:43:07 +10:00
|
|
|
function handleDragStart(event) {
|
2020-07-18 15:21:33 +10:00
|
|
|
const tokenGroup = event.target;
|
2020-05-22 20:43:07 +10:00
|
|
|
|
2021-04-23 17:10:10 +10:00
|
|
|
if (tokenState.category === "vehicle") {
|
2021-06-12 20:53:40 +10:00
|
|
|
const tokenIntersection = new Intersection(
|
|
|
|
|
getScaledOutline(tokenState, tokenWidth, tokenHeight),
|
|
|
|
|
{ x: tokenX - tokenWidth / 2, y: tokenY - tokenHeight / 2 },
|
|
|
|
|
{ x: tokenX, y: tokenY },
|
|
|
|
|
tokenState.rotation
|
|
|
|
|
);
|
2021-01-02 09:41:18 +11:00
|
|
|
|
2020-05-22 20:43:07 +10:00
|
|
|
// Find all other tokens on the map
|
2020-07-18 15:21:33 +10:00
|
|
|
const layer = tokenGroup.getLayer();
|
2020-08-27 19:09:16 +10:00
|
|
|
const tokens = layer.find(".character");
|
2020-05-22 20:43:07 +10:00
|
|
|
for (let other of tokens) {
|
2020-07-18 15:21:33 +10:00
|
|
|
if (other === tokenGroup) {
|
2020-05-22 20:43:07 +10:00
|
|
|
continue;
|
|
|
|
|
}
|
2021-06-12 20:53:40 +10:00
|
|
|
if (tokenIntersection.intersects(other.position())) {
|
2020-05-22 20:43:07 +10:00
|
|
|
// Save and restore token position after moving layer
|
|
|
|
|
const position = other.absolutePosition();
|
2020-07-18 15:21:33 +10:00
|
|
|
other.moveTo(tokenGroup);
|
2020-05-22 20:43:07 +10:00
|
|
|
other.absolutePosition(position);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onTokenDragStart(event);
|
|
|
|
|
}
|
|
|
|
|
|
2020-08-07 12:55:16 +10:00
|
|
|
function handleDragMove(event) {
|
|
|
|
|
const tokenGroup = event.target;
|
|
|
|
|
// Snap to corners of grid
|
|
|
|
|
if (map.snapToGrid) {
|
2021-02-09 14:13:08 +11:00
|
|
|
tokenGroup.position(snapPositionToGrid(tokenGroup.position()));
|
2020-08-07 12:55:16 +10:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-05-21 16:46:50 +10:00
|
|
|
function handleDragEnd(event) {
|
2020-07-18 15:21:33 +10:00
|
|
|
const tokenGroup = event.target;
|
2020-05-22 20:43:07 +10:00
|
|
|
|
2020-05-22 22:17:30 +10:00
|
|
|
const mountChanges = {};
|
2021-04-23 17:10:10 +10:00
|
|
|
if (tokenState.category === "vehicle") {
|
2020-07-20 19:14:46 +10:00
|
|
|
const parent = tokenGroup.getParent();
|
2020-08-27 19:09:16 +10:00
|
|
|
const mountedTokens = tokenGroup.find(".character");
|
2020-05-22 20:43:07 +10:00
|
|
|
for (let mountedToken of mountedTokens) {
|
|
|
|
|
// Save and restore token position after moving layer
|
|
|
|
|
const position = mountedToken.absolutePosition();
|
2020-07-20 19:14:46 +10:00
|
|
|
mountedToken.moveTo(parent);
|
2020-05-22 20:43:07 +10:00
|
|
|
mountedToken.absolutePosition(position);
|
2020-05-22 22:17:30 +10:00
|
|
|
mountChanges[mountedToken.id()] = {
|
2020-05-22 20:43:07 +10:00
|
|
|
x: mountedToken.x() / mapWidth,
|
|
|
|
|
y: mountedToken.y() / mapHeight,
|
2020-08-27 17:30:17 +10:00
|
|
|
lastModifiedBy: userId,
|
|
|
|
|
lastModified: Date.now(),
|
2020-05-22 22:17:30 +10:00
|
|
|
};
|
2020-05-22 20:43:07 +10:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-05-21 21:31:15 +10:00
|
|
|
setPreventMapInteraction(false);
|
2020-05-21 16:46:50 +10:00
|
|
|
onTokenStateChange({
|
2020-05-22 22:17:30 +10:00
|
|
|
...mountChanges,
|
|
|
|
|
[tokenState.id]: {
|
2020-07-18 15:21:33 +10:00
|
|
|
x: tokenGroup.x() / mapWidth,
|
|
|
|
|
y: tokenGroup.y() / mapHeight,
|
2020-08-27 17:30:17 +10:00
|
|
|
lastModifiedBy: userId,
|
|
|
|
|
lastModified: Date.now(),
|
2020-05-22 22:17:30 +10:00
|
|
|
},
|
2020-05-21 16:46:50 +10:00
|
|
|
});
|
2020-05-22 20:43:07 +10:00
|
|
|
onTokenDragEnd(event);
|
2020-05-21 16:46:50 +10:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleClick(event) {
|
2020-05-22 15:11:18 +10:00
|
|
|
if (draggable) {
|
|
|
|
|
const tokenImage = event.target;
|
|
|
|
|
onTokenMenuOpen(tokenState.id, tokenImage);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const [tokenOpacity, setTokenOpacity] = useState(1);
|
2020-08-28 17:27:19 +10:00
|
|
|
// Store token pointer down time to check for a click when token is locked
|
|
|
|
|
const tokenPointerDownTimeRef = useRef();
|
2020-08-27 17:30:17 +10:00
|
|
|
function handlePointerDown(event) {
|
2020-05-22 15:11:18 +10:00
|
|
|
if (draggable) {
|
|
|
|
|
setPreventMapInteraction(true);
|
|
|
|
|
}
|
2020-08-27 17:30:17 +10:00
|
|
|
if (tokenState.locked && map.owner === userId) {
|
2020-08-28 17:27:19 +10:00
|
|
|
tokenPointerDownTimeRef.current = event.evt.timeStamp;
|
2020-08-27 17:30:17 +10:00
|
|
|
}
|
2020-05-22 15:11:18 +10:00
|
|
|
}
|
|
|
|
|
|
2020-08-27 17:30:17 +10:00
|
|
|
function handlePointerUp(event) {
|
2020-05-22 15:11:18 +10:00
|
|
|
if (draggable) {
|
|
|
|
|
setPreventMapInteraction(false);
|
|
|
|
|
}
|
2020-08-27 17:30:17 +10:00
|
|
|
// Check token click when locked and we are the map owner
|
|
|
|
|
// We can't use onClick because that doesn't check pointer distance
|
|
|
|
|
if (tokenState.locked && map.owner === userId) {
|
2020-08-28 17:27:19 +10:00
|
|
|
// If down and up time is small trigger a click
|
|
|
|
|
const delta = event.evt.timeStamp - tokenPointerDownTimeRef.current;
|
|
|
|
|
if (delta < 300) {
|
2020-08-27 17:30:17 +10:00
|
|
|
const tokenImage = event.target;
|
|
|
|
|
onTokenMenuOpen(tokenState.id, tokenImage);
|
|
|
|
|
}
|
|
|
|
|
}
|
2020-05-22 15:11:18 +10:00
|
|
|
}
|
|
|
|
|
|
2020-07-31 14:50:01 +10:00
|
|
|
function handlePointerEnter() {
|
|
|
|
|
if (fadeOnHover) {
|
2020-05-22 15:11:18 +10:00
|
|
|
setTokenOpacity(0.5);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-07-31 14:50:01 +10:00
|
|
|
function handlePointerLeave() {
|
2020-06-27 10:04:54 +10:00
|
|
|
if (tokenOpacity !== 1.0) {
|
2020-05-22 15:11:18 +10:00
|
|
|
setTokenOpacity(1.0);
|
|
|
|
|
}
|
2020-05-21 16:46:50 +10:00
|
|
|
}
|
|
|
|
|
|
2021-02-07 11:16:36 +11:00
|
|
|
const minCellSize = Math.min(
|
|
|
|
|
gridCellPixelSize.width,
|
|
|
|
|
gridCellPixelSize.height
|
|
|
|
|
);
|
|
|
|
|
const tokenWidth = minCellSize * tokenState.size;
|
|
|
|
|
const tokenHeight = (minCellSize / tokenAspectRatio) * tokenState.size;
|
2020-04-24 15:50:05 +10:00
|
|
|
|
2020-05-22 22:17:30 +10:00
|
|
|
// Animate to new token positions if edited by others
|
|
|
|
|
const tokenX = tokenState.x * mapWidth;
|
|
|
|
|
const tokenY = tokenState.y * mapHeight;
|
|
|
|
|
const previousWidth = usePrevious(mapWidth);
|
|
|
|
|
const previousHeight = usePrevious(mapHeight);
|
|
|
|
|
const resized = mapWidth !== previousWidth || mapHeight !== previousHeight;
|
2020-08-27 17:30:17 +10:00
|
|
|
const skipAnimation = tokenState.lastModifiedBy === userId || resized;
|
2020-05-25 10:37:28 +10:00
|
|
|
const props = useSpring({
|
|
|
|
|
x: tokenX,
|
|
|
|
|
y: tokenY,
|
|
|
|
|
immediate: skipAnimation,
|
|
|
|
|
});
|
2020-05-21 22:57:44 +10:00
|
|
|
|
2020-08-27 17:30:17 +10:00
|
|
|
// When a token is hidden if you aren't the map owner hide it completely
|
|
|
|
|
if (map && !tokenState.visible && map.owner !== userId) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2020-08-27 19:09:16 +10:00
|
|
|
// Token name is used by on click to find whether a token is a vehicle or prop
|
2020-08-27 17:58:41 +10:00
|
|
|
let tokenName = "";
|
2021-04-23 17:10:10 +10:00
|
|
|
if (tokenState) {
|
|
|
|
|
tokenName = tokenState.category;
|
2020-08-27 17:58:41 +10:00
|
|
|
}
|
|
|
|
|
if (tokenState && tokenState.locked) {
|
|
|
|
|
tokenName = tokenName + "-locked";
|
|
|
|
|
}
|
|
|
|
|
|
2020-04-13 00:24:03 +10:00
|
|
|
return (
|
2020-05-25 10:37:28 +10:00
|
|
|
<animated.Group
|
|
|
|
|
{...props}
|
2020-05-21 16:46:50 +10:00
|
|
|
width={tokenWidth}
|
|
|
|
|
height={tokenHeight}
|
2020-05-22 15:11:18 +10:00
|
|
|
draggable={draggable}
|
|
|
|
|
onMouseDown={handlePointerDown}
|
|
|
|
|
onMouseUp={handlePointerUp}
|
2020-07-31 14:50:01 +10:00
|
|
|
onMouseEnter={handlePointerEnter}
|
|
|
|
|
onMouseLeave={handlePointerLeave}
|
2020-05-22 15:11:18 +10:00
|
|
|
onTouchStart={handlePointerDown}
|
|
|
|
|
onTouchEnd={handlePointerUp}
|
2020-05-21 20:57:52 +10:00
|
|
|
onClick={handleClick}
|
2020-05-22 23:55:50 +10:00
|
|
|
onTap={handleClick}
|
2020-05-21 20:57:52 +10:00
|
|
|
onDragEnd={handleDragEnd}
|
2020-05-22 20:43:07 +10:00
|
|
|
onDragStart={handleDragStart}
|
2020-08-07 12:55:16 +10:00
|
|
|
onDragMove={handleDragMove}
|
2020-08-27 17:30:17 +10:00
|
|
|
opacity={tokenState.visible ? tokenOpacity : 0.5}
|
2020-08-27 17:58:41 +10:00
|
|
|
name={tokenName}
|
2020-05-22 20:43:07 +10:00
|
|
|
id={tokenState.id}
|
2020-05-21 20:57:52 +10:00
|
|
|
>
|
2021-06-10 11:16:47 +10:00
|
|
|
<Group
|
|
|
|
|
width={tokenWidth}
|
|
|
|
|
height={tokenHeight}
|
|
|
|
|
x={0}
|
|
|
|
|
y={0}
|
|
|
|
|
rotation={tokenState.rotation}
|
|
|
|
|
offsetX={tokenWidth / 2}
|
|
|
|
|
offsetY={tokenHeight / 2}
|
|
|
|
|
>
|
2021-06-12 20:53:40 +10:00
|
|
|
<TokenOutline
|
|
|
|
|
outline={getScaledOutline(tokenState, tokenWidth, tokenHeight)}
|
|
|
|
|
hidden={!!tokenImage}
|
|
|
|
|
/>
|
2021-06-10 11:16:47 +10:00
|
|
|
</Group>
|
|
|
|
|
<KonvaImage
|
|
|
|
|
width={tokenWidth}
|
|
|
|
|
height={tokenHeight}
|
|
|
|
|
x={0}
|
|
|
|
|
y={0}
|
|
|
|
|
image={tokenImage}
|
|
|
|
|
rotation={tokenState.rotation}
|
|
|
|
|
offsetX={tokenWidth / 2}
|
|
|
|
|
offsetY={tokenHeight / 2}
|
|
|
|
|
hitFunc={() => {}}
|
|
|
|
|
/>
|
2020-05-22 21:10:05 +10:00
|
|
|
<Group offsetX={tokenWidth / 2} offsetY={tokenHeight / 2}>
|
2021-06-17 11:39:41 +10:00
|
|
|
{tokenState.statuses?.length > 0 ? (
|
|
|
|
|
<TokenStatus
|
|
|
|
|
tokenState={tokenState}
|
|
|
|
|
width={tokenWidth}
|
|
|
|
|
height={tokenHeight}
|
|
|
|
|
/>
|
|
|
|
|
) : null}
|
|
|
|
|
{tokenState.label ? (
|
|
|
|
|
<TokenLabel
|
|
|
|
|
tokenState={tokenState}
|
|
|
|
|
width={tokenWidth}
|
|
|
|
|
height={tokenHeight}
|
|
|
|
|
/>
|
|
|
|
|
) : null}
|
2020-05-22 21:10:05 +10:00
|
|
|
</Group>
|
2020-05-25 10:37:28 +10:00
|
|
|
</animated.Group>
|
2020-04-13 00:24:03 +10:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default MapToken;
|