import * as THREE from 'three' ; import memoizeOne from 'memoize-one' ; import { v4 } from 'uuid' ; import stringHash from 'string-hash' ; import { clamp } from 'lodash' ; import copyToClipboard from 'copy-to-clipboard' ; import { AnyProperties , castMapProperties , castMiniProperties , castTemplateProperties , defaultMapProperties , defaultMiniProperties , DriveMetadata , GridType , isTemplateMetadata , isTemplateProperties , MapProperties , MiniProperties , PieceVisibilityEnum , ScenarioObjectProperties , TemplateProperties } from './googleDriveUtils' ; import { CommsStyle } from './commsNode' ; import * as constants from './constants' ; import { MINI_HEIGHT , MINI_WIDTH } from './constants' ; import { TabletopPathPoint } from '../presentation/tabletopPathComponent' ; import { ConnectedUserUsersType } from '../redux/connectedUserReducer' ; import { buildEuler , buildVector3 , isColourDark , reverseEuler } from './threeUtils' ; import { isCloseTo } from './mathsUtils' ; import { PaintToolEnum } from '../presentation/paintTools' ; export interface WithMetadataType < T extends AnyProperties > { metadata : DriveMetadata < void , T > ; } export interface ObjectVector2 { x : number ; y : number ; } export interface ObjectVector3 { x : number ; y : number ; z : number ; } export interface ObjectEuler { x : number ; y : number ; z : number ; order : string ; _x ? : number ; _y ? : number ; _z ? : number ; _order ? : string ; } export interface MapPaintOperation { operationId : string ; selected : PaintToolEnum ; points : ObjectVector2 [ ] ; brushSize : number ; brushColour : string ; } export interface MapPaintLayer { operations : MapPaintOperation [ ] ; } export interface MapType extends WithMetadataType < MapProperties > { name : string ; position : ObjectVector3 ; rotation : ObjectEuler ; gmOnly : boolean ; selectedBy : string | null ; fogOfWar ? : number [ ] ; cameraFocusPoint ? : ObjectVector3 ; paintLayers : MapPaintLayer [ ] ; transparent : boolean ; } export type MovementPathPoint = ObjectVector3 & { elevation ? : number ; onMapId ? : string ; } ; export const MINI_VISIBILITY_OPTIONS = [ { displayName : 'Hide' , value : PieceVisibilityEnum . HIDDEN } , { displayName : 'Fog' , value : PieceVisibilityEnum . FOGGED } , { displayName : 'Show' , value : PieceVisibilityEnum . REVEALED } ] ; export type PiecesRosterValue = string | number | PiecesRosterFractionValue | boolean [ ] ; export type PiecesRosterValues = { [ columnId ] : PiecesRosterValue | undefined ; } ; export interface MiniType < T = MiniProperties | TemplateProperties > extends WithMetadataType < T > { name : string ; position : ObjectVector3 ; movementPath ? : MovementPathPoint [ ] ; rotation : ObjectEuler ; scale : number ; elevation : number ; visibility : PieceVisibilityEnum ; gmOnly : boolean ; selectedBy : string | null ; prone : boolean ; flat : boolean ; locked : boolean ; attachMiniId ? : string ; hideBase : boolean ; baseColour ? : number ; onMapId ? : string ; piecesRosterValues : PiecesRosterValues ; piecesRosterGMValues : PiecesRosterValues ; piecesRosterSimple : boolean ; gmNoteMarkdown ? : string ; } export interface ScenarioType { snapToGrid : boolean ; confirmMoves : boolean ; maps : { [ key ] : MapType ; } ; minis : { [ key ] : MiniType ; } ; startCameraAtOrigin ? : boolean ; headActionIds : string [ ] ; playerHeadActionIds : string [ ] ; } export enum DistanceMode { STRAIGHT = 'STRAIGHT' , GRID_DIAGONAL_ONE_ONE = 'GRID_DIAGONAL_ONE_ONE' , GRID_DIAGONAL_THREE_EVERY_TWO = 'GRID_DIAGONAL_THREE_EVERY_TWO' } export enum DistanceRound { ONE_DECIMAL = 'ONE_DECIMAL' , ROUND_OFF = 'ROUND_OFF' , ROUND_UP = 'ROUND_UP' , ROUND_DOWN = 'ROUND_DOWN' } export interface TabletopUserPreferencesType { dieColour : string ; } export interface TabletopUserControlType { whitelist : string [ ] ; blacklist : string [ ] ; } export enum PiecesRosterColumnType { INTRINSIC = 'intrinsic' , STRING = 'string' , NUMBER = 'number' , BONUS = 'bonus' , FRACTION = 'fraction' } export interface PiecesRosterBaseColumn { id : string ; name : string ; gmOnly : boolean ; showNear ? : boolean ; } export interface PiecesRosterIntrinsicColumn { type : PiecesRosterColumnType . INTRINSIC ; } export interface PiecesRosterStringColumn { type : PiecesRosterColumnType . STRING ; } export interface PiecesRosterNumberColumn { type : PiecesRosterColumnType . NUMBER | PiecesRosterColumnType . BONUS ; } export interface PiecesRosterFractionValue { numerator ? : number ; denominator : number ; } export interface PiecesRosterFractionColumn { type : PiecesRosterColumnType . FRACTION ; } export type PiecesRosterColumn = PiecesRosterBaseColumn & ( PiecesRosterIntrinsicColumn | PiecesRosterStringColumn | PiecesRosterNumberColumn | PiecesRosterFractionColumn ) ; export interface TabletopType { gm : string ; gmSecret : string | null ; gmOnlyPing : boolean ; defaultGrid : GridType ; distanceMode : DistanceMode ; distanceRound : DistanceRound ; gridScale ? : number ; gridUnit ? : string ; commsStyle : CommsStyle ; baseColourSwatches ? : string [ ] ; templateColourSwatches ? : string [ ] ; gridColourSwatches ? : string [ ] ; paintToolColourSwatches ? : string [ ] ; tabletopLockedPeerId ? : string ; tabletopUserControl ? : TabletopUserControlType ; lastSavedHeadActionIds : null | string [ ] ; lastSavedPlayerHeadActionIds : null | string [ ] ; videoMuted : { [ metadataId ] : boolean | undefined ; } ; userPreferences : { [ key ] : TabletopUserPreferencesType ; } ; piecesRosterColumns : PiecesRosterColumn [ ] ; } function replaceMetadataWithId ( all ) { return Object . keys ( all ) . reduce ( ( result , guid ) => { result [ guid ] = { ... all [ guid ] , metadata : { id : all [ guid ] . metadata . id , resourceKey : all [ guid ] . metadata . resourceKey } } ; return result ; } , { } ) ; } function filterObject < T > ( object , filterFn ) { return Object . keys ( object ) . reduce ( ( result , key ) => { const filtered = filterFn ( object [ key ] ) ; if ( filtered ) { result [ key ] = filtered ; } return result ; } , { } ) ; } export function scenarioToJson ( scenario ) { const maps = replaceMetadataWithId ( scenario . maps ) ; const minis = replaceMetadataWithId ( scenario . minis ) ; return [ { snapToGrid : scenario . snapToGrid , confirmMoves : scenario . confirmMoves , startCameraAtOrigin : scenario . startCameraAtOrigin , maps , minis , headActionIds : scenario . playerHeadActionIds , playerHeadActionIds : scenario . playerHeadActionIds } , { snapToGrid : scenario . snapToGrid , confirmMoves : scenario . confirmMoves , startCameraAtOrigin : scenario . startCameraAtOrigin , maps : filterObject ( maps , ( map ) => ( map . gmOnly ? undefined : map ) ) , minis : filterObject ( minis , ( mini ) => ( mini . gmOnly ? undefined : { ... mini , piecesRosterGMValues : { } , gmNoteMarkdown : undefined } ) ) , headActionIds : scenario . headActionIds , playerHeadActionIds : scenario . playerHeadActionIds } ] ; } function updateMetadata < T = ScenarioObjectProperties > ( fullDriveMetadata , object , converter ) { Object . keys ( object ) . forEach ( ( id ) => { const metadata = fullDriveMetadata [ object [ id ] . metadata . id ] as DriveMetadata < void , T > ; if ( metadata ) { object [ id ] = { ... object [ id ] , metadata : { ... metadata , properties : converter ( metadata . properties ) } } ; } } ) ; } export const INITIAL_PIECES_ROSTER_COLUMNS = [ { name : 'Name' , id : v4 ( ) , gmOnly : false , showNear : true , type : PiecesRosterColumnType . INTRINSIC } , { name : 'Focus' , id : v4 ( ) , gmOnly : false , type : PiecesRosterColumnType . INTRINSIC } , { name : 'Visibility' , id : v4 ( ) , gmOnly : true , type : PiecesRosterColumnType . INTRINSIC } , { name : 'Locked' , id : v4 ( ) , gmOnly : true , type : PiecesRosterColumnType . INTRINSIC } ] ; export function jsonToScenarioAndTabletop ( combined , fullDriveMetadata ) { Object . keys ( combined . minis ) . forEach ( ( miniId ) => { const mini = combined . minis [ miniId ] ; if ( mini [ 'startingPosition' ] ) { mini . movementPath = [ mini [ 'startingPosition' ] ] ; delete ( mini [ 'startingPosition' ] ) ; } if ( mini . visibility === undefined ) { mini . visibility = ( mini . gmOnly ) ? PieceVisibilityEnum . HIDDEN : PieceVisibilityEnum . REVEALED ; } } ) ; Object . keys ( combined . maps ) . forEach ( ( mapId ) => { const map = combined . maps [ mapId ] ; if ( ! map . paintLayers ) { map . paintLayers = [ ] ; } } ) ; updateMetadata ( fullDriveMetadata , combined . maps , castMapProperties ) ; updateMetadata ( fullDriveMetadata , combined . minis , castMiniProperties ) ; const headActionIds = combined . headActionIds ? combined . headActionIds : [ combined [ 'lastActionId' ] || 'legacyAction' ] ; const playerHeadActionIds = combined . playerHeadActionIds ? combined . playerHeadActionIds : [ combined [ 'lastActionId' ] || 'legacyAction' ] ; const piecesRosterColumns = combined . piecesRosterColumns || INITIAL_PIECES_ROSTER_COLUMNS ; const nameColumn = piecesRosterColumns . find ( isNameColumn ) ; if ( nameColumn && nameColumn . showNear === undefined ) { nameColumn . showNear = true ; } return [ { snapToGrid : combined . snapToGrid , confirmMoves : combined . confirmMoves , startCameraAtOrigin : combined . startCameraAtOrigin , maps : combined . maps , minis : combined . minis , headActionIds , playerHeadActionIds } , { gm : combined . gm , gmSecret : combined . gmSecret , gmOnlyPing : combined . gmOnlyPing === undefined ? false : combined . gmOnlyPing , defaultGrid : combined . defaultGrid || GridType . SQUARE , distanceMode : combined . distanceMode , distanceRound : combined . distanceRound , gridScale : combined . gridScale , gridUnit : combined . gridUnit , commsStyle : combined . commsStyle || CommsStyle . PeerToPeer , baseColourSwatches : combined . baseColourSwatches , templateColourSwatches : combined . templateColourSwatches , gridColourSwatches : combined . gridColourSwatches , paintToolColourSwatches : combined . paintToolColourSwatches , lastSavedHeadActionIds : null , lastSavedPlayerHeadActionIds : null , tabletopLockedPeerId : combined . tabletopLockedPeerId , tabletopUserControl : combined . tabletopUserControl , videoMuted : combined . videoMuted || { } , userPreferences : combined . userPreferences || { } , piecesRosterColumns } ] ; } export function getAllScenarioMetadataIds ( scenario ) { const metadataMap = Object . keys ( scenario . maps ) . reduce ( ( all , mapId ) => { all [ scenario . maps [ mapId ] . metadata . id ] = true ; return all ; } , Object . keys ( scenario . minis ) . reduce ( ( all , miniId ) => { all [ scenario . minis [ miniId ] . metadata . id ] = true ; return all ; } , { } ) ) ; return Object . keys ( metadataMap ) ; } function isAboveHexDiagonal ( coordStraight , coordZigzag , hexStraight , hexZigzag , hexStraightSize , hexZigzagSize ) { if ( ( hexZigzag % 3 ) !== 0 ) { return false ; } else if ( ( hexStraight + hexZigzag ) & 1 ) { return ( coordZigzag < hexZigzagSize / 3 * ( coordStraight / hexStraightSize + hexZigzag - hexStraight ) ) ; } else { return ( coordZigzag < hexZigzagSize / 3 * ( 1 + hexZigzag + hexStraight - coordStraight / hexStraightSize ) ) ; } } export function getGridStride ( type , half = true ) { switch ( type ) { case GridType . HEX_VERT : return half ? { strideX : constants . SQRT3 / 2 , strideY : 0.5 } : { strideX : constants . SQRT3 , strideY : 1 } ; case GridType . HEX_HORZ : return half ? { strideX : 0.5 , strideY : constants . SQRT3 / 2 } : { strideX : 1 , strideY : constants . SQRT3 } ; default : return { strideX : 1 , strideY : 1 } ; } } export function getHexCoordinatesCentreOffset ( type ) { switch ( type ) { case GridType . HEX_HORZ : return { centreX : 1 , centreY : 2 / 3 } ; case GridType . HEX_VERT : return { centreX : 2 / 3 , centreY : 1 } ; } } export function cartesianToHexCoords ( x , y , type ) { const { strideX , strideY } = getGridStride ( type ) ; let hexStraight , hexZigzag , above , hexX , hexY ; if ( type === GridType . HEX_VERT ) { hexZigzag = Math . floor ( 3 * x / strideX ) ; hexStraight = Math . floor ( y / strideY ) ; above = isAboveHexDiagonal ( y , x , hexStraight , hexZigzag , strideY , strideX ) ; } else { hexStraight = Math . floor ( x / strideX ) ; hexZigzag = Math . floor ( 3 * y / strideY ) ; above = isAboveHexDiagonal ( x , y , hexStraight , hexZigzag , strideX , strideY ) ; } hexZigzag = Math . floor ( hexZigzag / 3 ) ; if ( above ) { hexZigzag -- ; } if ( hexZigzag & 1 ) { hexStraight -= ( hexStraight & 1 ) ? 0 : 1 ; } else { hexStraight &= ~ 1 ; } if ( type === GridType . HEX_VERT ) { hexX = hexZigzag ; hexY = hexStraight ; } else { hexX = hexStraight ; hexY = hexZigzag ; } let { centreX , centreY } = getHexCoordinatesCentreOffset ( type ) ; centreX += hexX ; centreY += hexY ; return { strideX , strideY , hexX , hexY , centreX , centreY } ; } const MAP_ROTATION_SNAP = Math . PI / 2 ; const MAP_ROTATION_HEX_SNAP = Math . PI / 6 ; export function effectiveHexGridType ( mapRotation , gridType ) { if ( Math . round ( mapRotation / MAP_ROTATION_HEX_SNAP ) % 2 === 0 ) { return gridType ; } else if ( gridType === GridType . HEX_HORZ ) { return GridType . HEX_VERT ; } else { return GridType . HEX_HORZ ; } } export function getMapCentreOffsets ( snap , properties ) { const { strideX , strideY } = getGridStride ( properties . gridType , false ) ; const dx = ( strideX + properties . gridOffsetX / properties . gridSize ) % strideX ; const dy = ( strideY + properties . gridOffsetY / properties . gridSize ) % strideY ; let mapDX = 0 , mapDZ = 0 ; if ( snap ) { const mapCentreX = properties . width / 2 ; const mapCentreY = properties . height / 2 ; switch ( properties . gridType ) { case GridType . HEX_HORZ : case GridType . HEX_VERT : const { centreX , centreY } = getHexCoordinatesCentreOffset ( properties . gridType ) ; const { hexX , hexY } = cartesianToHexCoords ( mapCentreX + strideX / 2 * centreX - dx , mapCentreY + strideY / 2 * centreY - dy , properties . gridType ) ; mapDX = hexX * strideX / 2 + dx - mapCentreX ; mapDZ = hexY * strideY / 2 + dy - mapCentreY ; break ; default : mapDX = ( dx - mapCentreX ) % strideX ; mapDZ = ( dy - mapCentreY ) % strideY ; break ; } } return { dx , dy , mapDX , mapDZ } ; } export function snapMap ( snap , properties , position , rotation = { order : 'XYZ' , x : 0 , y : 0 , z : 0 } ) { if ( ! properties ) { return { positionObj : position , rotationObj : rotation , dx : 0 , dy : 0 , width : 10 , height : 10 } ; } const rotationSnap = ( properties . gridType === GridType . HEX_HORZ || properties . gridType === GridType . HEX_VERT ) ? MAP_ROTATION_HEX_SNAP : MAP_ROTATION_SNAP ; const { dx , dy , mapDX , mapDZ } = getMapCentreOffsets ( snap , properties ) ; if ( snap ) { const mapRotation = Math . round ( rotation . y / rotationSnap ) * rotationSnap ; const cos = Math . cos ( mapRotation ) ; const sin = Math . sin ( mapRotation ) ; let x , z ; switch ( properties . gridType ) { case GridType . HEX_HORZ : case GridType . HEX_VERT : const snapGridType = effectiveHexGridType ( mapRotation , properties . gridType ) ; const { strideX , strideY , centreX , centreY } = cartesianToHexCoords ( position . x - cos * mapDX - sin * mapDZ , position . z - cos * mapDZ + sin * mapDX , snapGridType ) ; x = centreX * strideX - cos * mapDX - sin * mapDZ ; z = centreY * strideY - cos * mapDZ + sin * mapDX ; break ; default : x = Math . round ( position . x + cos * mapDX + sin * mapDZ ) - cos * mapDX - sin * mapDZ ; z = Math . round ( position . z + cos * mapDZ - sin * mapDX ) - cos * mapDZ + sin * mapDX ; break ; } const y = Math . round ( + position . y ) ; return { positionObj : { x , y , z } , rotationObj : { ... rotation , y : mapRotation } , dx , dy , width : properties . width , height : properties . height } ; } else { return { positionObj : position , rotationObj : rotation , dx , dy , width : properties . width , height : properties . height } ; } } export function getAbsoluteMiniPosition ( miniId , minis , snap ? , gridType ? ) { if ( ! miniId || ! minis [ miniId ] ) { return undefined ; } let { position : positionObj , rotation : rotationObj , elevation , attachMiniId , selectedBy , scale } = minis [ miniId ] ; if ( attachMiniId ) { const baseMiniPosition = getAbsoluteMiniPosition ( attachMiniId , minis , snap , gridType ) ; if ( ! baseMiniPosition ) { return undefined ; } const { positionObj : attachedPosition , rotationObj : attachedRotation , elevation : attachedElevation } = baseMiniPosition ; positionObj = buildVector3 ( positionObj ) . applyEuler ( buildEuler ( attachedRotation ) ) . add ( attachedPosition as THREE . Vector3 ) ; rotationObj = { x : rotationObj . x + attachedRotation . x , y : rotationObj . y + attachedRotation . y , z : rotationObj . z + attachedRotation . z , order : rotationObj . order } ; elevation += attachedElevation ; } return ( snap && gridType ) ? snapMini ( snap && ! ! selectedBy , gridType , scale , positionObj , elevation , rotationObj ) : { positionObj , rotationObj , elevation } ; } const MINI_SQUARE_ROTATION_SNAP = Math . PI / 4 ; const MINI_HEX_ROTATION_SNAP = Math . PI / 3 ; export function snapMini ( snap , gridType , scaleFactor , position , elevation , rotation = { order : 'XYZ' , x : 0 , y : 0 , z : 0 } ) { if ( snap ) { const scale = scaleFactor > 1 ? Math . round ( scaleFactor ) : 1.0 / ( Math . round ( 1.0 / scaleFactor ) ) ; const gridSnap = scale > 1 ? 1 : scale ; let x , z ; let rotationSnap ; switch ( gridType ) { case GridType . HEX_HORZ : case GridType . HEX_VERT : const { strideX , strideY , centreX , centreY } = cartesianToHexCoords ( position . x / gridSnap , position . z / gridSnap , gridType ) ; x = centreX * strideX * gridSnap ; z = centreY * strideY * gridSnap ; rotationSnap = MINI_HEX_ROTATION_SNAP ; break ; default : const offset = ( scale / 2 ) % 1 ; x = Math . round ( ( position . x - offset ) / gridSnap ) * gridSnap + offset ; z = Math . round ( ( position . z - offset ) / gridSnap ) * gridSnap + offset ; rotationSnap = MINI_SQUARE_ROTATION_SNAP ; } const y = Math . round ( + position . y ) ; return { positionObj : { x , y , z } , rotationObj : { ... rotation , y : Math . round ( rotation . y / rotationSnap ) * rotationSnap } , scaleFactor : scale , elevation : Math . round ( elevation ) } ; } else { return { positionObj : position , rotationObj : rotation , scaleFactor , elevation } ; } } export function getGridTypeOfMap ( map ? , defaultGridType = GridType . NONE ) { if ( ! map || ! map . metadata . properties ) { return defaultGridType ; } const gridType = map . metadata . properties . gridType ; if ( gridType === GridType . HEX_VERT || gridType === GridType . HEX_HORZ ) { return effectiveHexGridType ( map . rotation . y , gridType ) ; } else { return gridType ; } } export function generateMovementPath ( movementPath , maps , defaultGridType ) { return movementPath . map ( ( point ) => { let gridType = defaultGridType ; if ( point . onMapId ) { const onMap = maps [ point . onMapId ] ; gridType = ( onMap && onMap . metadata . properties ) ? onMap . metadata . properties . gridType : defaultGridType ; if ( onMap && ( gridType === GridType . HEX_HORZ || gridType === GridType . HEX_VERT ) ) { gridType = effectiveHexGridType ( onMap . rotation . y , gridType ) ; } } return { x : point . x , y : point . y + ( point . elevation || 0 ) , z : point . z , gridType } ; } ) ; } const GRID_COLOUR_TO_HEX = { black : '#000000' , grey : '#9b9b9b' , white : '#ffffff' , brown : '#8b572a' , tan : '#c77f16' , red : '#ff0000' , yellow : '#ffff00' , green : '#00ff00' , cyan : '#00ffff' , blue : '#0000ff' , magenta : '#ff00ff' } ; export function getColourHex ( colour ) { if ( colour instanceof THREE . Color ) { return colour . getHex ( ) ; } else { const hex = GRID_COLOUR_TO_HEX [ colour ] || colour || '#000000' ; return Number . parseInt ( hex [ 0 ] === '#' ? hex . substr ( 1 ) : hex , 16 ) ; } } export function getColourHexString ( colour ) { if ( typeof ( colour ) === 'string' && colour [ 0 ] === '#' ) { colour = parseInt ( colour . slice ( 1 ) , 16 ) ; } const hexString = Number ( colour ) . toString ( 16 ) ; return '#' + ( '000000' + hexString ) . slice ( - 6 ) ; } export const getNetworkHubId = memoizeOne ( ( myUserId , myPeerId , gm , connectedUsers ) => { let networkHubId = ( myUserId === gm ) ? myPeerId : null ; for ( let peerId of Object . keys ( connectedUsers ) ) { if ( connectedUsers [ peerId ] . user . emailAddress === gm && ( ! networkHubId || peerId < networkHubId ) ) { networkHubId = peerId ; } } return networkHubId ; } ) ; export function * spiralSquareGridGenerator ( ) { let horizontal = true , step = 1 , delta = 1 , x = 0 , y = 0 ; while ( true ) { if ( horizontal ) { x += delta ; if ( 2 * x * delta >= step ) { horizontal = false ; } } else { y += delta ; if ( 2 * y * delta >= step ) { horizontal = true ; delta = - delta ; step ++ ; } } yield { x , y } ; } } const hexHorizontalGridPath = [ { dx : 1 , dy : 0 } , { dx : 0.5 , dy : 1.5 * constants . INV_SQRT3 } , { dx : - 0.5 , dy : 1.5 * constants . INV_SQRT3 } , { dx : - 1 , dy : 0 } , { dx : - 0.5 , dy : - 1.5 * constants . INV_SQRT3 } , { dx : 0.5 , dy : - 1.5 * constants . INV_SQRT3 } ] ; const hexVerticalGridPath = [ { dx : 1.5 * constants . INV_SQRT3 , dy : 0.5 } , { dx : 0 , dy : 1 } , { dx : - 1.5 * constants . INV_SQRT3 , dy : 0.5 } , { dx : - 1.5 * constants . INV_SQRT3 , dy : - 0.5 } , { dx : 0 , dy : - 1 } , { dx : 1.5 * constants . INV_SQRT3 , dy : - 0.5 } ] ; export function * spiralHexGridGenerator ( gridType ) { const path = ( gridType === GridType . HEX_HORZ ) ? hexHorizontalGridPath : hexVerticalGridPath ; let x = 0 , y = 0 , sideLength = 1 , direction = 0 ; while ( true ) { const { dx , dy } = path [ direction ] ; for ( let step = ( direction === 1 ) ? 1 : 0 ; step < sideLength ; ++ step ) { x += dx ; y += dy ; yield { x , y } ; } if ( ++ direction >= path . length ) { direction = 0 ; sideLength ++ ; } } } const ROUND_VECTORS_DELTA = 0.01 ; export function roundSquareVectors ( start , end ) { if ( start . x <= end . x ) { start . x = Math . floor ( start . x ) ; end . x = Math . ceil ( end . x + ROUND_VECTORS_DELTA ) - ROUND_VECTORS_DELTA ; } else { start . x = Math . ceil ( start . x + ROUND_VECTORS_DELTA ) - ROUND_VECTORS_DELTA ; end . x = Math . floor ( end . x ) ; } if ( start . z <= end . z ) { start . z = Math . floor ( start . z ) ; end . z = Math . ceil ( end . z + ROUND_VECTORS_DELTA ) - ROUND_VECTORS_DELTA ; } else { start . z = Math . ceil ( start . z + ROUND_VECTORS_DELTA ) - ROUND_VECTORS_DELTA ; end . z = Math . floor ( end . z ) ; } } function growHexVectors ( startPos , endPos , gridType , grow = 1 ) { const { centreX , centreY } = getHexCoordinatesCentreOffset ( gridType ) ; const xStartMore = ( startPos . x > endPos . x ? 0.98 : - 0.98 ) * grow ; startPos . x += centreX * xStartMore ; endPos . x -= centreX * xStartMore ; const zStartMore = ( startPos . z > endPos . z ? 0.98 : - 0.98 ) * grow ; startPos . z += centreY * zStartMore ; endPos . z -= centreY * zStartMore ; } param map param rotation param worldStart param worldEnd export function getMapGridRoundedVectors ( map , rotation , worldStart , worldEnd ) { const mapPosition = buildVector3 ( map . position ) ; const reverseRotation = reverseEuler ( rotation ) ; const startPos = buildVector3 ( worldStart ) . sub ( mapPosition ) . applyEuler ( reverseRotation ) . add ( mapPosition ) ; const endPos = buildVector3 ( worldEnd ) . sub ( mapPosition ) . applyEuler ( reverseRotation ) . add ( mapPosition ) ; const properties = castMapProperties ( map . metadata . properties ) ; const { mapDX , mapDZ } = getMapCentreOffsets ( true , properties ) ; const mapCentre = new THREE . Vector3 ( mapDX , 0 , mapDZ ) ; const centreOffset = mapPosition . clone ( ) . add ( mapCentre ) ; startPos . sub ( centreOffset ) ; endPos . sub ( centreOffset ) ; if ( properties . gridType === GridType . HEX_HORZ || properties . gridType === GridType . HEX_VERT ) { const { strideX , strideY } = getGridStride ( properties . gridType ) ; const { centreX , centreY } = getHexCoordinatesCentreOffset ( properties . gridType ) ; const { hexX : startHexX , hexY : startHexY } = cartesianToHexCoords ( startPos . x + strideX * centreX , startPos . z + strideY * centreY , properties . gridType ) ; const { hexX : endHexX , hexY : endHexY } = cartesianToHexCoords ( endPos . x + strideX * centreX , endPos . z + strideY * centreY , properties . gridType ) ; startPos . set ( startHexX , startPos . y , startHexY ) ; endPos . set ( endHexX , endPos . y , endHexY ) ; growHexVectors ( startPos , endPos , properties . gridType ) ; const hexScale = new THREE . Vector3 ( strideX , 1 , strideY ) ; startPos . multiply ( hexScale ) ; endPos . multiply ( hexScale ) ; } else { roundSquareVectors ( startPos , endPos ) ; } startPos . add ( mapCentre ) ; endPos . add ( mapCentre ) ; return { startPos , endPos } ; } export function getShaderFogOffsets ( gridType , dx , dy , mapWidth , mapHeight , fogWidth , fogHeight ? ) { const { strideX , strideY } = getGridStride ( gridType ) ; switch ( gridType ) { case GridType . HEX_HORZ : return { shaderDX : 2 * strideX - dx , shaderDY : fogHeight ? ( fogHeight * strideY - mapHeight - strideY * 5 / 3 + ( dy % strideY ) ) : dy } ; case GridType . HEX_VERT : return { shaderDX : 4 * strideX / 3 - dx , shaderDY : fogHeight ? ( fogHeight - mapHeight + dy ) % 1 : dy } ; default : return { shaderDX : strideX - dx , shaderDY : fogHeight ? ( fogHeight - mapHeight - strideY + dy ) : dy } ; } } /**
 * Return a vector with integer x and z (and zero y) giving the coordinates on the fog map that corresponds with the
 * calculated map centre of the map, and also a boolean indicating if the point on the fog map is offset (bumped left on
 * a horizontal hex grid, bumped down on a vertical one).
 */ export function getFogMapCentre ( properties ) { const { dx , dy , mapDX , mapDZ } = getMapCentreOffsets ( true , properties ) ; const { shaderDX , shaderDY } = getShaderFogOffsets ( properties . gridType , dx , dy , properties . width , properties . height , properties . fogWidth , properties . fogHeight ) ; let { strideX , strideY } = getGridStride ( properties . gridType , false ) ; switch ( properties . gridType ) { case GridType . HEX_HORZ : strideY /= 2 ; break ; case GridType . HEX_VERT : strideX /= 2 ; break ; } const fogMapCentre = new THREE . Vector3 ( Math . floor ( ( properties . width / 2 + shaderDX + mapDX ) / strideX + 0.1 ) , 0 , Math . floor ( properties . fogHeight - ( properties . height / 2 + shaderDY - mapDZ ) / strideY + 0.1 ) ) ; let isCentreOffset = false ; switch ( properties . gridType ) { case GridType . HEX_HORZ : isCentreOffset = ( fogMapCentre . z % 2 ) === 1 ; break ; case GridType . HEX_VERT : isCentreOffset = ( fogMapCentre . x % 2 ) === 1 ; if ( isCentreOffset ) { fogMapCentre . z -- ; } break ; } return { fogMapCentre , isCentreOffset } ; } param map param start param end export function getMapFogRect ( map , start , end ) { const rotation = buildEuler ( map . rotation ) ; const properties = castMapProperties ( map . metadata . properties ) ; const { startPos , endPos } = getMapGridRoundedVectors ( map , rotation , start , end ) ; const { mapDX , mapDZ } = getMapCentreOffsets ( true , properties ) ; const mapCentre = new THREE . Vector3 ( mapDX , 0 , mapDZ ) ; startPos . sub ( mapCentre ) ; endPos . sub ( mapCentre ) ; const { fogMapCentre , isCentreOffset } = getFogMapCentre ( properties ) ; if ( properties . gridType === GridType . HEX_HORZ || properties . gridType === GridType . HEX_VERT ) { const { strideX , strideY } = getGridStride ( properties . gridType ) ; const hexScale = new THREE . Vector3 ( strideX , 1 , strideY ) ; startPos . divide ( hexScale ) ; endPos . divide ( hexScale ) ; growHexVectors ( startPos , endPos , properties . gridType , - 1 ) ; if ( properties . gridType === GridType . HEX_HORZ ) { const fogOriginBump = isCentreOffset ? 0 : 1 ; startPos . x = Math . floor ( ( Math . round ( startPos . x ) + fogOriginBump ) / 2 ) ; endPos . x = Math . floor ( ( Math . round ( endPos . x ) + fogOriginBump ) / 2 ) ; } else { const fogOriginBump = isCentreOffset ? 1 : 0 ; startPos . z = Math . floor ( ( Math . round ( startPos . z ) + fogOriginBump ) / 2 ) ; endPos . z = Math . floor ( ( Math . round ( endPos . z ) + fogOriginBump ) / 2 ) ; } } startPos . add ( fogMapCentre ) ; endPos . add ( fogMapCentre ) ; const startX = clamp ( Math . round ( Math . min ( startPos . x , endPos . x ) ) , 0 , properties . fogWidth ) ; const startY = clamp ( Math . round ( Math . min ( startPos . z , endPos . z ) ) , 0 , properties . fogHeight ) ; const endX = Math . max ( startX , clamp ( Math . floor ( Math . max ( startPos . x , endPos . x ) ) , 0 , properties . fogWidth ) ) ; const endY = Math . max ( startY , clamp ( Math . floor ( Math . max ( startPos . z , endPos . z ) ) , 0 , properties . fogHeight ) ) ; return { startX , startY , endX , endY , fogWidth : properties . fogWidth , fogHeight : properties . fogHeight } ; } export function getUpdatedMapFogRect ( map , start , end , reveal ) { let { startX , startY , endX , endY , fogWidth , fogHeight } = getMapFogRect ( map , start , end ) ; let fogOfWar = map . fogOfWar ? [ ... map . fogOfWar ] : new Array ( Math . ceil ( fogWidth * fogHeight / 32.0 ) ) . fill ( - 1 ) ; const gridType = map . metadata . properties . gridType ; const singlePoint = ( startX === endX && startY === endY ) ; const startXOdd = ( startX % 2 ) === 1 ; const endXOdd = ( endX % 2 ) === 1 ; const startYOdd = ( startY % 2 ) === 1 ; const endYOdd = ( endY % 2 ) === 1 ; const pointsSouthEast = ( ( ( startX === endX && startYOdd ) || start . x < end . x ) === ( ( startY === endY && ! startXOdd ) || start . z < end . z ) ) ; const combTop = gridType !== GridType . HEX_VERT || singlePoint || ( startXOdd ? ( ! endXOdd && ! pointsSouthEast ) : ( ! endXOdd || pointsSouthEast ) ) ? undefined : 0 ; let combBottom = gridType !== GridType . HEX_VERT || singlePoint || ( startXOdd ? ( endXOdd || ! pointsSouthEast ) : ( endXOdd && pointsSouthEast ) ) ? undefined : 1 ; const combLeft = gridType !== GridType . HEX_HORZ || singlePoint || ( startYOdd ? ( endYOdd || pointsSouthEast ) : ( endYOdd && ! pointsSouthEast ) ) ? undefined : 1 ; let combRight = gridType !== GridType . HEX_HORZ || singlePoint || ( startYOdd ? ( ! endYOdd && pointsSouthEast ) : ( ! endYOdd || ! pointsSouthEast ) ) ? undefined : 0 ; if ( ! singlePoint ) { if ( gridType === GridType . HEX_VERT && startY === endY ) { if ( startX % 2 ) { combBottom = 1 ; endY ++ ; } else { combBottom = undefined ; } } else if ( gridType === GridType . HEX_HORZ && startX === endX ) { if ( startY % 2 ) { combRight = undefined ; } else { combRight = 0 ; endX ++ ; } } } for ( let y = startY ; y <= endY ; ++ y ) { for ( let x = startX ; x <= endX ; ++ x ) { if ( ( y === startY && x % 2 === combTop ) || ( y === endY && x % 2 === combBottom ) || ( x === startX && y % 2 === combLeft ) || ( x === endX && y % 2 === combRight ) ) { continue ; } const textureIndex = x + y * fogWidth ; const bitmaskIndex = textureIndex >> 5 ; const mask = 1 << ( textureIndex & 0x1f ) ; if ( reveal === null ) { fogOfWar [ bitmaskIndex ] ^= mask ; } else if ( reveal ) { fogOfWar [ bitmaskIndex ] |= mask ; } else { fogOfWar [ bitmaskIndex ] &= ~ mask ; } } } return fogOfWar ; } export function isMapFoggedAtPosition ( map , position , fogOfWar = map ? map . fogOfWar || null : null ) { if ( map && fogOfWar ) { const { startX : mapX , startY : mapY , fogWidth , fogHeight } = getMapFogRect ( map , position , position ) ; if ( mapX < 0 || mapX >= fogWidth || mapY < 0 || mapY >= fogHeight ) { return false ; } const textureIndex = mapX + mapY * fogWidth ; const bitmaskIndex = textureIndex >> 5 ; const mask = 1 << ( textureIndex & 0x1f ) ; return ( fogOfWar [ bitmaskIndex ] & mask ) === 0 ; } return false ; } export function getMapIdAtPoint ( point , maps , allowHidden ) { return Object . keys ( maps ) . reduce < string | undefined > ( ( touching , mapId ) => { const map = maps [ mapId ] ; if ( touching || ( ! allowHidden && map . gmOnly ) || ! isCloseTo ( point . y , map . position . y ) ) { return touching ; } const { width , height } = castMapProperties ( map . metadata . properties ) ; const cos = Math . cos ( + map . rotation . y ) ; const sin = Math . sin ( + map . rotation . y ) ; const dx = point . x - map . position . x ; const dz = point . z - map . position . z ; const effectiveX = dx * cos - dz * sin ; const effectiveZ = dz * cos + dx * sin ; return ( effectiveX >= - width / 2 && effectiveX < width / 2 && effectiveZ >= - height / 2 && effectiveZ < height / 2 ) ? mapId : touching ; } , undefined ) ; } export function getRootAttachedMiniId ( miniId , minis ) { while ( minis [ miniId ] . attachMiniId ) { miniId = minis [ miniId ] . attachMiniId ! ; } return miniId ; } export function isTabletopLockedForPeer ( tabletop , connectedUsers , peerId , override = false ) { const fromGm = ( override && peerId ) ? ( connectedUsers [ peerId ] && connectedUsers [ peerId ] . verifiedConnection && connectedUsers [ peerId ] . user . emailAddress === tabletop . gm ) : false ; return ! ! ( tabletop . tabletopLockedPeerId && tabletop . tabletopLockedPeerId !== peerId && ! fromGm ) ; } export function isScenarioEmpty ( scenario ? ) { return ! scenario || ( Object . keys ( scenario . minis ) . length === 0 && Object . keys ( scenario . maps ) . length === 0 ) ; } export const SAME_LEVEL_MAP_DELTA_Y = 1.5 ; export const NEW_MAP_DELTA_Y = 6.0 ; export const MAP_EPSILON = 0.01 ; export const isMapIdHighest = memoizeOne ( ( maps , mapId ? ) : boolean => { const map = mapId ? maps [ mapId ] : undefined ; return ! map ? true : Object . keys ( maps ) . reduce < boolean > ( ( highest , otherMapId ) => { return highest && ( mapId === otherMapId || maps [ otherMapId ] . position . y <= map . position . y + SAME_LEVEL_MAP_DELTA_Y ) ; } , true ) ; } ) ; export const isMapIdLowest = memoizeOne ( ( maps , mapId ? ) : boolean => { const map = mapId ? maps [ mapId ] : undefined ; return ! map ? true : Object . keys ( maps ) . reduce < boolean > ( ( lowest , otherMapId ) => { return lowest && ( mapId === otherMapId || maps [ otherMapId ] . position . y > map . position . y - SAME_LEVEL_MAP_DELTA_Y ) ; } , true ) ; } ) ; export const getMapIdClosestToZero = memoizeOne ( ( maps ) => { let closestElevation = 0 ; return Object . keys ( maps ) . reduce < string | undefined > ( ( closestId , mapId ) => { const elevation = Math . abs ( + maps [ mapId ] . position . y ) ; if ( closestId === undefined || elevation < closestElevation || ( elevation === closestElevation && mapId < closestId ) ) { closestElevation = elevation ; return mapId ; } else { return closestId ; } } , undefined ) ; } ) ; export function arePositionsOnSameLevel ( position1 , position2 ) { return Math . abs ( position1 . y - position2 . y ) <= SAME_LEVEL_MAP_DELTA_Y ; } export const getMapIdsAtLevel = memoizeOne ( ( maps , elevation ) => { return Object . keys ( maps ) . filter ( ( mapId ) => { const map = maps [ mapId ] ; return map . position . y >= elevation - SAME_LEVEL_MAP_DELTA_Y && map . position . y <= elevation + SAME_LEVEL_MAP_DELTA_Y ; } ) ; } ) ; param maps param elevation returns { focusMapId function _getFocusMapIdAndFocusPointAtLevel ( maps , elevation ? ) { if ( elevation === undefined ) { const closestId = getMapIdClosestToZero ( maps ) ; elevation = closestId ? maps [ closestId ] . position . y : 0 ; } const levelMapIds = getMapIdsAtLevel ( maps , elevation ) ; let focusMapId = undefined ; let cameraFocusMapId = undefined ; for ( let mapId of levelMapIds ) { const map = maps [ mapId ] ; focusMapId = ( ! focusMapId || map . position . y > maps [ focusMapId ] . position . y || mapId < focusMapId ) ? mapId : focusMapId ; cameraFocusMapId = ( map . cameraFocusPoint && ( ! cameraFocusMapId || map . cameraFocusPoint . y > maps [ cameraFocusMapId ] . cameraFocusPoint ! . y || mapId < cameraFocusMapId ) ) ? mapId : cameraFocusMapId ; } let cameraFocusPoint = undefined ; if ( focusMapId && cameraFocusMapId ) { const pointMap = maps [ cameraFocusMapId ] ; const cameraFocusOffset = pointMap . cameraFocusPoint ! ; cameraFocusPoint = { x : pointMap . position . x + cameraFocusOffset . x , y : maps [ focusMapId ] . position . y , z : pointMap . position . z + cameraFocusOffset . z } ; } return { focusMapId , cameraFocusPoint } ; } export const getFocusMapIdAndFocusPointAtLevel = memoizeOne ( _getFocusMapIdAndFocusPointAtLevel ) ; param direction param maps param mapId param limit export function getMapIdOnNextLevel ( direction , maps , mapId ? , limit = true ) { const mapY = mapId && maps [ mapId ] ? maps [ mapId ] . position . y : 0 ; const floor = direction > 0 ? mapY + SAME_LEVEL_MAP_DELTA_Y : ( limit ? mapY - NEW_MAP_DELTA_Y : undefined ) ; const ceiling = direction > 0 ? ( limit ? mapY + NEW_MAP_DELTA_Y : undefined ) : mapY - SAME_LEVEL_MAP_DELTA_Y ; return Object . keys ( maps ) . reduce < string | undefined > ( ( result , otherMapId ) => { const mapY = maps [ otherMapId ] . position . y ; return ( floor === undefined || mapY >= floor ) && ( ceiling === undefined || mapY <= ceiling ) && ( ! result || ( direction > 0 && mapY < maps [ result ] . position . y ) || ( direction < 0 && mapY > maps [ result ] . position . y ) ) ? otherMapId : result ; } , undefined ) ; } function _getHighestMapId ( maps ) { return Object . keys ( maps ) . reduce < string | undefined > ( ( maxMapId , mapId ) => ( maxMapId === undefined || maps [ maxMapId ] . position . y < maps [ mapId ] . position . y ? mapId : maxMapId ) , undefined ) ; } export const getHighestMapId = memoizeOne ( _getHighestMapId ) ; function adjustMapPositionToNotCollide ( scenario , position , properties , performAdjust ) { let adjusted = false ; for ( let mapId of Object . keys ( scenario . maps ) ) { const map = scenario . maps [ mapId ] ; const { width : mapWidth , height : mapHeight } = castMapProperties ( map . metadata . properties ) ; if ( arePositionsOnSameLevel ( position , map . position ) && position . x + properties . width / 2 >= map . position . x - mapWidth / 2 && position . x - properties . width / 2 < map . position . x + mapWidth / 2 && position . z + properties . height / 2 >= map . position . z - mapHeight / 2 && position . z - properties . height / 2 < map . position . z + mapHeight / 2 ) { adjusted = true ; if ( performAdjust ) { const delta = position . clone ( ) . sub ( map . position as THREE . Vector3 ) ; const quadrant14 = ( delta . x - delta . z > 0 ) ; const quadrant12 = ( delta . x + delta . z > 0 ) ; if ( quadrant12 && quadrant14 ) { position . x = map . position . x + MAP_EPSILON + ( mapWidth + properties . width ) / 2 ; } else if ( quadrant12 ) { position . z = map . position . z + MAP_EPSILON + ( mapHeight + properties . height ) / 2 ; } else if ( quadrant14 ) { position . z = map . position . z - MAP_EPSILON - ( mapHeight + properties . height ) / 2 ; } else { position . x = map . position . x - MAP_EPSILON - ( mapWidth + properties . width ) / 2 ; } const { positionObj } = snapMap ( true , properties , position ) ; position . copy ( positionObj as THREE . Vector3 ) ; } } } return adjusted ; } export function findPositionForNewMap ( scenario , rawProperties , position ) { let properties = castMapProperties ( rawProperties ) ; properties = { ... properties , width : properties . width || 10 , height : properties . height || 10 } ; const { positionObj } = snapMap ( true , properties , position ) ; while ( true ) { const search = buildVector3 ( positionObj ) ; if ( getMapIdAtPoint ( search , scenario . maps , true ) === undefined ) { adjustMapPositionToNotCollide ( scenario , search , properties , true ) ; if ( ! adjustMapPositionToNotCollide ( scenario , search , properties , false ) ) { return search ; } } positionObj . y += NEW_MAP_DELTA_Y ; } } function _getMaxCameraDistance ( maps ) { const maxMapDimension = Object . keys ( maps ) . reduce ( ( max , mapId ) => { const { width , height } = maps [ mapId ] . metadata . properties || { width : 10 , height : 10 } ; return Math . max ( max , width , height ) ; } , 0 ) ; return Math . max ( 2 * maxMapDimension , 50 ) ; } export const getMaxCameraDistance = memoizeOne ( _getMaxCameraDistance ) ; const CAMERA_INITIAL_OFFSET = new THREE . Vector3 ( 0 , Math . sqrt ( 0.5 ) , Math . sqrt ( 0.5 ) ) ; function _getBaseCameraParameters ( map ? , zoom = 1 , cameraLookAt ? ) { cameraLookAt = cameraLookAt || buildVector3 ( map ? map . position : { x : 0 , y : 0 , z : 0 } ) ; const { width , height } = ( map ?. metadata . properties ?. width ) ? map . metadata . properties : { width : 10 , height : 10 } ; const cameraDistance = 2 * Math . max ( 20 , width , height ) ; const cameraPosition = cameraLookAt . clone ( ) . addScaledVector ( CAMERA_INITIAL_OFFSET , zoom * Math . sqrt ( cameraDistance ) ) ; return { cameraLookAt , cameraPosition } ; } export const getBaseCameraParameters = memoizeOne ( _getBaseCameraParameters ) ; export function isUserAllowedOnTabletop ( gm , email , tabletopUserControl ? ) { if ( email !== gm && tabletopUserControl ) { const onWhitelist = tabletopUserControl . whitelist . reduce ( ( match , value ) => ( ( value === email || ( ! match && value === '*' ) ) ? value : match ) , '' ) ; const onBlacklist = tabletopUserControl . blacklist . reduce ( ( match , value ) => ( ( value === email || ( ! match && value === '*' ) ) ? value : match ) , '' ) ; if ( ! onWhitelist && ! onBlacklist ) { return null ; } else if ( ! onWhitelist || onBlacklist === onWhitelist || onBlacklist === email ) { return false ; } } return true ; } export function getVisibilityString ( visibility ) { const option = MINI_VISIBILITY_OPTIONS . find ( ( option ) => ( option . value === visibility ) ) ; return option ? option . displayName : '' ; } export function isNameColumn ( column ) { return column . type === PiecesRosterColumnType . INTRINSIC && column . name === 'Name' ; } export const intrinsicFieldValueMap = { Name : ( mini ) => ( mini . name ) , Focus : ( ) => ( '' ) , Visibility : ( mini ) => ( mini . visibility === PieceVisibilityEnum . FOGGED ? ( mini . gmOnly ? 'Fog (hide)' : 'Fog (show)' ) : getVisibilityString ( mini . visibility ) ) , Locked : ( mini ) => ( mini . locked ? 'Y' : 'N' ) , Attached : ( mini , minis ) => ( mini . attachMiniId ? 'to ' + minis [ mini . attachMiniId ] . name : '' ) , Prone : ( mini ) => ( mini . prone ? 'Y' : 'N' ) , Flat : ( mini ) => ( mini . flat ? 'Y' : 'N' ) , Base : ( mini ) => ( mini . hideBase ? 'N' : 'Y' ) , Scale : ( mini ) => ( mini . scale . toString ( 10 ) ) , Template : ( mini ) => ( isTemplateMetadata ( mini . metadata ) ? 'Template' : 'Miniature' ) } ; const intrinsicFieldSortKeyMap = { Visibility : ( mini ) => { switch ( mini . visibility ) { case PieceVisibilityEnum . REVEALED : return '1' ; case PieceVisibilityEnum . FOGGED : return mini . gmOnly ? '4' : '2' ; case PieceVisibilityEnum . HIDDEN : return '3' ; } } } ; export function getPiecesRosterValue ( column , mini , minis ) { const values = ( column . gmOnly ? mini . piecesRosterGMValues : mini . piecesRosterValues ) || { } ; const value = values [ column . id ] ; switch ( column . type ) { case PiecesRosterColumnType . INTRINSIC : return intrinsicFieldValueMap [ column . name ] ? intrinsicFieldValueMap [ column . name ] ( mini , minis ) : '' ; case PiecesRosterColumnType . STRING : return value === undefined ? '' : value ; case PiecesRosterColumnType . NUMBER : return value === undefined ? 0 : value ; case PiecesRosterColumnType . BONUS : const bonus = value === undefined ? 0 : value ; return bonus < 0 ? String ( bonus ) : '+' + String ( bonus ) ; case PiecesRosterColumnType . FRACTION : return ( value === undefined ? { denominator : 0 } : value ) as PiecesRosterFractionValue ; } } export function getPiecesRosterDisplayValue ( column , values ) { const value = values [ column . id ] ; const header = column . name + ': ' ; switch ( column . type ) { case PiecesRosterColumnType . STRING : return ! value ? '' : header + value ; case PiecesRosterColumnType . NUMBER : return header + ( value === undefined ? '0' : String ( value ) ) ; case PiecesRosterColumnType . BONUS : const bonus = value === undefined ? 0 : value ; return header + ( bonus < 0 ? String ( bonus ) : '+' + String ( bonus ) ) ; case PiecesRosterColumnType . FRACTION : const fraction = value as PiecesRosterFractionValue ; const { numerator , denominator } = value === undefined ? { numerator : 0 , denominator : 0 } : fraction . numerator === undefined ? { numerator : fraction . denominator , denominator : fraction . denominator } : fraction ; return denominator === 0 ? ( numerator === 0 ? `${ header }full` : `${ header }${ numerator ! > 0 ? 'up' : 'down' } ${ Math . abs ( numerator ! ) }` ) : ( `${ header }${ numerator } / ${ denominator }` ) ; default : return '' ; } } export function getPiecesRosterSortString ( column , mini , minis ) { if ( column . type === PiecesRosterColumnType . INTRINSIC ) { if ( intrinsicFieldSortKeyMap [ column . name ] ) { return intrinsicFieldSortKeyMap [ column . name ] ( mini ) ; } } else if ( mini . piecesRosterSimple ) { return '' ; } const value = getPiecesRosterValue ( column , mini , minis ) ; if ( column . type === PiecesRosterColumnType . FRACTION ) { const fraction = value as PiecesRosterFractionValue ; return ( ( fraction . denominator === 0 ) ? 0 : fraction . numerator === undefined ? 1 : fraction . numerator / fraction . denominator ) . toString ( ) + ' ' + fraction . denominator ; } else { return value === undefined ? '' : value . toString ( ) ; } } export function getUserDiceColours ( tabletop , email ) { let diceColour ; if ( tabletop . userPreferences [ email ] ) { diceColour = tabletop . userPreferences [ email ] . dieColour ; } else { diceColour = getColourHexString ( Math . floor ( stringHash ( email ) / 2 ) ) ; } const textColour = isColourDark ( new THREE . Color ( diceColour ) ) ? 'white' : 'black' ; return { diceColour , textColour } ; } export function adjustScenarioOrigin ( scenario , defaultGrid , origin , orientation ) { scenario . maps = Object . keys ( scenario . maps ) . reduce ( ( maps , mapId ) => { const map = scenario . maps [ mapId ] ; const position = buildVector3 ( map . position ) . applyEuler ( orientation ) . add ( origin ) ; const rotation = { ... map . rotation , y : map . rotation . y + orientation . y } ; const { positionObj , rotationObj } = snapMap ( true , map . metadata . properties , position , rotation ) ; maps [ mapId ] = { ... map , position : positionObj , rotation : rotationObj } ; return maps ; } , { } ) ; scenario . minis = Object . keys ( scenario . minis ) . reduce ( ( minis , miniId ) => { const mini = scenario . minis [ miniId ] ; if ( mini . attachMiniId ) { minis [ miniId ] = mini ; } else { const position = buildVector3 ( mini . position ) . applyEuler ( orientation ) . add ( origin ) ; const rotation = { ... mini . rotation , y : mini . rotation . y + orientation . y } ; const gridType = mini . onMapId ? getGridTypeOfMap ( scenario . maps [ mini . onMapId ] ) : defaultGrid ; const { positionObj , rotationObj , elevation } = snapMini ( scenario . snapToGrid , gridType , mini . scale , position , mini . elevation , rotation ) ; minis [ miniId ] = { ... mini , position : positionObj , rotation : rotationObj , elevation } ; } return minis ; } , { } ) ; return scenario ; } export function copyURLToClipboard ( suffix ) { const location = window . location . href . replace ( /[\\/][^/\\]*$/ , '/' + suffix ) ; copyToClipboard ( location ) ; } export function calculatePieceProperties < T extends MiniProperties | TemplateProperties > ( previous , update = { } ) { if ( isTemplateProperties ( previous ) ) { return castTemplateProperties ( { ... previous , ... update } ) as T ; } else { return calculateMiniProperties ( previous , update as Partial < MiniProperties > ) as T ; } } const MINI_ASPECT_RATIO = MINI_WIDTH / MINI_HEIGHT ; export function calculateMiniProperties ( previous , update = { } ) { const cleaned = castMiniProperties ( { ... defaultMiniProperties , ... previous } ) ; for ( let key of Object . keys ( cleaned ) ) { if ( typeof ( cleaned [ key ] ) !== 'string' && isNaN ( cleaned [ key ] ) ) { delete ( cleaned [ key ] ) ; } } const combined = { ... cleaned as Partial < MiniProperties > , ... update } as MiniProperties ; if ( Number ( combined . width ) !== Number ( cleaned . width ) || Number ( combined . height ) !== Number ( cleaned . height ) ) { const aspectRatio = Number ( combined . width ) / Number ( combined . height ) ; const topDownX = ( aspectRatio > 1 ) ? 0.5 : aspectRatio / 2 ; const topDownY = ( aspectRatio > 1 ) ? 0.5 / aspectRatio : 0.5 ; const standeeRangeX = ( aspectRatio > MINI_ASPECT_RATIO ? MINI_WIDTH : MINI_HEIGHT * aspectRatio ) ; const standeeRangeY = ( aspectRatio > MINI_ASPECT_RATIO ? MINI_WIDTH / aspectRatio : MINI_HEIGHT ) ; const standeeX = 0.5 ; const standeeY = ( 1 - MINI_HEIGHT / standeeRangeY ) / 2 ; return { ... combined , topDownX , topDownY , aspectRatio , standeeRangeX , standeeRangeY , standeeX , standeeY } ; } else { return combined ; } } export function calculateMapProperties ( previous , update = { } ) { const cleaned = castMapProperties ( { ... defaultMapProperties , ... previous } ) ; for ( let key of Object . keys ( cleaned ) ) { if ( typeof ( cleaned [ key ] ) !== 'string' && isNaN ( cleaned [ key ] ) ) { delete ( cleaned [ key ] ) ; } } if ( update . width !== undefined ) { update . width /= ( update . gridSize || cleaned . gridSize ) ; } if ( update . height !== undefined ) { update . height /= ( update . gridSize || cleaned . gridSize ) ; } return { ... cleaned , ... update } ; } export function mapMetadataHasNoGrid ( metadata ? ) { const gridType = metadata ?. properties ?. gridType ; return gridType === undefined || gridType === GridType . NONE ; }