import * as THREE from 'three' ; import { Action , AnyAction , combineReducers , Reducer } from 'redux' ; import { Omit } from 'react-redux' ; import { v4 } from 'uuid' ; import { GroupByFunction } from 'redux-undo' ; import { pick } from 'lodash' ; import { objectMapReducer } from './genericReducers' ; import { FileIndexActionTypes , RemoveFileActionType , UpdateFileActionType } from './fileIndexReducer' ; import { getAbsoluteMiniPosition , getMapCentreOffsets , getRootAttachedMiniId , isMapFoggedAtPosition , mapMetadataHasNoGrid , MapPaintOperation , MapType , MiniType , MovementPathPoint , ObjectEuler , ObjectVector3 , PiecesRosterColumn , PiecesRosterValue , scenarioToJson , ScenarioType , snapMap } from '../util/scenarioUtils' ; import { getScenarioFromStore , getUndoableHistoryFromStore , ReduxStoreType } from './mainReducer' ; import { buildEuler , buildVector3 , eulerToObject , vector3ToObject } from '../util/threeUtils' ; import { castMapProperties , castMiniProperties , DriveMetadata , MapProperties , MiniProperties , PieceVisibilityEnum , ScenarioObjectProperties , TemplateProperties } from '../util/googleDriveUtils' ; import { ConnectedUserActionTypes } from './connectedUserReducer' ; import { GToveThunk , isScenarioAction , ScenarioAction } from '../util/types' ; import { TabletopReducerActionTypes } from './tabletopReducer' ; export enum ScenarioReducerActionTypes { SET_SCENARIO_ACTION = 'set-scenario-action' , APPEND_SCENARIO_ACTION = 'append-scenario-action' , SET_SCENARIO_LOCAL_ACTION = 'set-scenario-local-action' , UPDATE_MAP_ACTION = 'update-map-action' , UPDATE_MINI_ACTION = 'update-mini-action' , ADJUST_MINIS_ON_MAP_ACTION = 'adjust-minis-on-map-action' , REMOVE_MAP_ACTION = 'remove-map-action' , REMOVE_MINI_ACTION = 'remove-mini-action' , UPDATE_SNAP_TO_GRID_ACTION = 'update-snap-to-grid-action' , REPLACE_METADATA_ACTION = 'replace-metadata-action' , REPLACE_MAP_IMAGE_ACTION = 'replace-map-image-action' , UPDATE_CONFIRM_MOVES_ACTION = 'update-confirm-moves-action' , UPDATE_HEAD_ACTION_IDS = 'update-head-action-ids' } type PartialBy < T , K extends keyof T > = Omit < T , K > & Partial < Pick < T , K > > ; function populateScenarioAction < T extends ScenarioAction > ( action , getState ) { const scenario = getScenarioFromStore ( getState ( ) ) ; const gmOnly = action . gmOnly || false ; return Object . assign ( { } , action , { actionId : v4 ( ) , headActionIds : gmOnly ? scenario . headActionIds : scenario . playerHeadActionIds , gmOnly } ) as T ; } function populateScenarioActionThunk < T extends ScenarioAction > ( action ) { return ( dispatch , getState ) => { const fullAction = populateScenarioAction ( action , getState ) ; return dispatch ( fullAction ) ; } ; } interface SetScenarioAction extends ScenarioAction { type : ScenarioReducerActionTypes . SET_SCENARIO_ACTION ; scenario : Partial < ScenarioType > ; } export function setScenarioAction ( scenario , peerKey , gmOnly = false , playersOnly ? ) { return populateScenarioActionThunk ( { type : ScenarioReducerActionTypes . SET_SCENARIO_ACTION , scenario , peerKey , gmOnly , playersOnly } ) ; } interface AppendScenarioAction extends ScenarioAction { type : ScenarioReducerActionTypes . APPEND_SCENARIO_ACTION ; scenario : Partial < ScenarioType > ; } export function appendScenarioAction ( scenario , peerKey , gmOnly = false ) { return populateScenarioActionThunk ( { type : ScenarioReducerActionTypes . APPEND_SCENARIO_ACTION , scenario , peerKey , gmOnly } ) ; } export interface SetScenarioLocalAction { type : ScenarioReducerActionTypes . SET_SCENARIO_LOCAL_ACTION ; scenario : ScenarioType ; } export function setScenarioLocalAction ( scenario ) { return { type : ScenarioReducerActionTypes . SET_SCENARIO_LOCAL_ACTION , scenario } ; } interface UpdateSnapToGridActionType extends ScenarioAction { type : ScenarioReducerActionTypes . UPDATE_SNAP_TO_GRID_ACTION ; snapToGrid : boolean ; } export function updateSnapToGridAction ( snapToGrid ) { return populateScenarioActionThunk ( { type : ScenarioReducerActionTypes . UPDATE_SNAP_TO_GRID_ACTION , snapToGrid , peerKey : 'snapToGrid' } ) ; } interface UpdateConfirmMovesActionType extends ScenarioAction { type : ScenarioReducerActionTypes . UPDATE_CONFIRM_MOVES_ACTION ; confirmMoves : boolean ; } export function updateConfirmMovesAction ( confirmMoves ) { return populateScenarioActionThunk ( { type : ScenarioReducerActionTypes . UPDATE_CONFIRM_MOVES_ACTION , confirmMoves , peerKey : 'confirmMoves' } ) ; } interface RemoveMapActionType extends ScenarioAction { type : ScenarioReducerActionTypes . REMOVE_MAP_ACTION ; mapId : string ; } export function removeMapAction ( mapId ) { return undoGroupThunk ( ( dispatch , getState ) => { const gmOnly = getGmOnly ( { getState , mapId } ) ; const scenario = getScenarioFromStore ( getState ( ) ) ; for ( let miniId of Object . keys ( scenario . minis ) ) { const mini = scenario . minis [ miniId ] ; if ( mini . onMapId === mapId && mini . visibility === PieceVisibilityEnum . FOGGED && mini . gmOnly ) { dispatch ( updateMiniGMOnlyAction ( miniId , false ) as any ) ; } } dispatch ( populateScenarioAction ( { type : ScenarioReducerActionTypes . REMOVE_MAP_ACTION , mapId , peerKey : mapId , gmOnly } , getState ) ) ; } ) ; } interface UpdateMapActionType extends ScenarioAction { type : ScenarioReducerActionTypes . UPDATE_MAP_ACTION ; mapId : string ; map : Partial < MapType > ; } export function addMapAction ( mapParameter , mapId = v4 ( ) ) { const map = { ... initialMapState , fogOfWar : mapMetadataHasNoGrid ( mapParameter . metadata ) ? undefined : [ ] , ... mapParameter } ; return populateScenarioActionThunk ( { type : ScenarioReducerActionTypes . UPDATE_MAP_ACTION , mapId , map , peerKey : mapId , gmOnly : map . gmOnly } ) ; } function updateMapAction ( mapId , map , selectedBy , extra = '' ) { return ( dispatch , getState ) => { let undoGroupId = null ; if ( extra === fogOfWarExtra ) { undoGroupId = v4 ( ) ; const scenario = getScenarioFromStore ( getState ( ) ) ; const oldMap = scenario . maps [ mapId ] ; for ( let miniId of Object . keys ( scenario . minis ) ) { const mini = scenario . minis [ miniId ] ; if ( mini . onMapId === mapId && mini . visibility === PieceVisibilityEnum . FOGGED ) { let rootMiniId = getRootAttachedMiniId ( miniId , scenario . minis ) ; const onFog = isMapFoggedAtPosition ( oldMap , scenario . minis [ rootMiniId ] . position , map . fogOfWar || null ) ; if ( mini . gmOnly !== onFog ) { dispatch ( undoGroupThunk ( updateMiniGMOnlyAction ( miniId , onFog ) , undoGroupId ) as any ) ; } } } } dispatch ( undoGroupAction ( populateScenarioAction ( { type : ScenarioReducerActionTypes . UPDATE_MAP_ACTION , mapId , map : { ... map , selectedBy } , peerKey : mapId + extra , gmOnly : getGmOnly ( { getState , mapId } ) } , getState ) , undoGroupId ) ) ; } ; } export function updateMapPositionAction ( mapId , position , selectedBy ) { return updateMapAction ( mapId , { position : vector3ToObject ( position ) } , selectedBy , 'position' ) ; } export function updateMapRotationAction ( mapId , rotation , selectedBy ) { return updateMapAction ( mapId , { rotation : eulerToObject ( rotation ) } , selectedBy , 'rotation' ) ; } const fogOfWarExtra = 'fogOfWar' ; export function updateMapFogOfWarAction ( mapId , fogOfWar ? ) { return updateMapAction ( mapId , { fogOfWar } , null , fogOfWarExtra ) ; } export function updateMapCameraFocusPoint ( mapId , cameraFocusPoint ? ) { const map = { cameraFocusPoint : cameraFocusPoint ? vector3ToObject ( cameraFocusPoint ) : undefined } ; return updateMapAction ( mapId , map , null , 'cameraFocus' ) ; } export function updateMapGMOnlyAction ( mapId , gmOnly ) { return ( dispatch , getState ) => { const scenario = getScenarioFromStore ( getState ( ) ) ; const map = { ... scenario . maps [ mapId ] , gmOnly } ; if ( gmOnly ) { dispatch ( populateScenarioAction < RemoveMapActionType > ( { type : ScenarioReducerActionTypes . REMOVE_MAP_ACTION , mapId , peerKey : mapId , gmOnly : false } , getState ) ) ; dispatch ( populateScenarioAction < UpdateMapActionType > ( { type : ScenarioReducerActionTypes . UPDATE_MAP_ACTION , mapId , peerKey : mapId , map , gmOnly : true } , getState ) ) ; } else { dispatch ( populateScenarioAction < UpdateMapActionType > ( { type : ScenarioReducerActionTypes . UPDATE_MAP_ACTION , mapId , map , peerKey : mapId , gmOnly : false } , getState ) ) ; } } ; } export function updateMapPaintLayerAction ( mapId , layerIndex , operationIndex , operation ) { return ( dispatch , getState ) => { const scenario = getScenarioFromStore ( getState ( ) ) ; const prevMap = scenario . maps [ mapId ] ; const paintLayers = [ ... prevMap . paintLayers ] ; paintLayers [ layerIndex ] = prevMap . paintLayers [ layerIndex ] ? { operations : [ ... prevMap . paintLayers [ layerIndex ] . operations ] } : { operations : [ ] } ; paintLayers [ layerIndex ] . operations [ operationIndex ] = operation ; dispatch ( populateScenarioAction ( { type : ScenarioReducerActionTypes . UPDATE_MAP_ACTION , mapId , map : { paintLayers } , peerKey : operation . operationId + 'paint' , gmOnly : getGmOnly ( { getState , mapId } ) } , getState ) ) ; } ; } export function clearMapPaintLayerAction ( mapId ) { return updateMapAction ( mapId , { paintLayers : [ ] } , null , 'paintClear' ) ; } export function updateMapTransparencyAction ( mapId , transparent ) { return updateMapAction ( mapId , { transparent } , null , 'transparent' ) ; } interface RemoveMiniActionType extends ScenarioAction { type : ScenarioReducerActionTypes . REMOVE_MINI_ACTION ; miniId : string ; positionObj ? : ObjectVector3 ; rotationObj ? : ObjectEuler ; elevation ? : number ; } export function removeMiniAction ( miniId , playersOnly ? ) { return ( dispatch , getState ) => { const scenario = getScenarioFromStore ( getState ( ) ) ; const absoluteMiniPosition = getAbsoluteMiniPosition ( miniId , scenario . minis ) ; if ( ! absoluteMiniPosition ) { dispatch ( populateScenarioAction ( { type : ScenarioReducerActionTypes . REMOVE_MINI_ACTION , miniId , playersOnly , peerKey : 'remove' + miniId , gmOnly : getGmOnly ( { getState , miniId } ) } , getState ) ) ; } else { const { positionObj , rotationObj , elevation } = absoluteMiniPosition ; dispatch ( populateScenarioAction ( { type : ScenarioReducerActionTypes . REMOVE_MINI_ACTION , miniId , positionObj , rotationObj , elevation , playersOnly , peerKey : 'remove' + miniId , gmOnly : getGmOnly ( { getState , miniId } ) } , getState ) ) ; } } ; } interface UpdateMiniActionType extends ScenarioAction { type : ScenarioReducerActionTypes . UPDATE_MINI_ACTION ; miniId : string ; mini : Partial < MiniType > ; } export function addMiniAction ( miniParameter ) { const miniId = v4 ( ) ; const mini = { position : ORIGIN , rotation : ROTATION_NONE , scale : 1.0 , elevation : 0.0 , gmOnly : true , prone : false , flat : false , piecesRosterValues : { } , piecesRosterGMValues : { } , ... miniParameter } ; return populateScenarioActionThunk ( { type : ScenarioReducerActionTypes . UPDATE_MINI_ACTION , miniId , mini , peerKey : miniId , gmOnly : mini . gmOnly } ) ; } function updateMiniAction ( miniId , mini , selectedBy , extra = '' ) { return ( dispatch , getState ) => { const prevState = getState ( ) ; const prevScenario = getScenarioFromStore ( prevState ) ; const prevMini = prevScenario . minis [ miniId ] ; if ( typeof ( mini ) === 'function' ) { mini = mini ( prevState ) ; } if ( mini . visibility || mini . position || extra === attachExtra ) { const visibility = mini . visibility || ( prevMini && prevMini . visibility ) ; let gmOnly = ( visibility === PieceVisibilityEnum . HIDDEN ) ; if ( visibility === PieceVisibilityEnum . FOGGED ) { const onMapId = mini . onMapId || ( prevMini && prevMini . onMapId ) ; let rootMiniId = getRootAttachedMiniId ( miniId , prevScenario . minis ) ; const position = rootMiniId === miniId ? ( mini . position || ( prevMini && prevMini . position ) ) : prevScenario . minis [ rootMiniId ] . position ; gmOnly = onMapId ? isMapFoggedAtPosition ( prevScenario . maps [ onMapId ] , position ) : false ; } if ( prevMini && prevMini . gmOnly !== gmOnly ) { mini . gmOnly = gmOnly ; for ( let otherMiniId of Object . keys ( prevScenario . minis ) ) { if ( otherMiniId !== miniId && prevScenario . minis [ otherMiniId ] . visibility === PieceVisibilityEnum . FOGGED && getRootAttachedMiniId ( otherMiniId , prevScenario . minis ) === miniId ) { dispatch ( updateMiniGMOnlyAction ( otherMiniId , gmOnly ) as any ) ; } } } } const updated = { ... prevMini , ... mini } ; if ( prevMini && updated . attachMiniId !== prevMini . attachMiniId ) { mini = { ... mini , movementPath : ! prevScenario . confirmMoves ? undefined : [ getCurrentPositionWaypoint ( prevMini , mini ) ] } ; } if ( prevMini ) { if ( ! prevMini . gmOnly && mini . gmOnly === true ) { dispatch ( removeMiniAction ( miniId , true ) as any ) ; } else if ( prevMini . gmOnly && mini . gmOnly === false ) { dispatch ( populateScenarioAction < UpdateMiniActionType > ( { type : ScenarioReducerActionTypes . UPDATE_MINI_ACTION , miniId , peerKey : 'add' + miniId , mini : { ... prevMini , gmOnly : false } , playersOnly : true } , getState ) ) ; } } dispatch ( populateScenarioAction ( { type : ScenarioReducerActionTypes . UPDATE_MINI_ACTION , miniId , mini : { ... mini , selectedBy } , peerKey : miniId + extra , gmOnly : ( mini . gmOnly !== undefined ? mini . gmOnly : prevMini . gmOnly ) || ( mini . piecesRosterGMValues !== undefined || mini . gmNoteMarkdown !== undefined ) } , getState ) ) ; } ; } export function updateMiniNameAction ( miniId , name ) { return updateMiniAction ( miniId , { name } , null , 'name' ) ; } export function updateMiniPositionAction ( miniId , position , selectedBy , onMapId ) { return updateMiniAction ( miniId , { position : vector3ToObject ( position ) , onMapId } , selectedBy , 'position' ) ; } export function updateMiniRotationAction ( miniId , rotation , selectedBy ) { return updateMiniAction ( miniId , { rotation : eulerToObject ( rotation ) } , selectedBy , 'rotation' ) ; } export function updateMiniScaleAction ( miniId , scale , selectedBy ) { return updateMiniAction ( miniId , { scale } , selectedBy , 'scale' ) ; } export function updateMiniElevationAction ( miniId , elevation , selectedBy ) { return updateMiniAction ( miniId , { elevation } , selectedBy , 'elevation' ) ; } export function updateMiniLockedAction ( miniId , locked ) { return updateMiniAction ( miniId , { locked } , null , 'locked' ) ; } export function updateMiniProneAction ( miniId , prone ) { return updateMiniAction ( miniId , { prone } , null , 'prone' ) ; } export function updateMiniFlatAction ( miniId , flat ) { return updateMiniAction ( miniId , { flat } , null , 'flat' ) ; } export function updateMiniHideBaseAction ( miniId , hideBase ) { return updateMiniAction ( miniId , { hideBase } , null , 'hideBase' ) ; } export function updateMiniBaseColourAction ( miniId , baseColour ) { return updateMiniAction ( miniId , { baseColour } , null , 'baseColour' ) ; } const attachExtra = 'attach' ; export function updateAttachMinisAction ( miniId , attachMiniId , position , rotation , elevation ) { return updateMiniAction ( miniId , { attachMiniId , position , rotation , elevation } , null , attachExtra ) ; } export function confirmMiniMoveAction ( miniId ) { return updateMiniAction ( miniId , ( state ) => ( { movementPath : [ getCurrentPositionWaypoint ( getScenarioFromStore ( state ) . minis [ miniId ] ) ] } ) , null , 'movementPath' ) ; } export function addMiniWaypointAction ( miniId ) { return updateMiniAction ( miniId , ( state ) => { const mini = getScenarioFromStore ( state ) . minis [ miniId ] ; return ( mini . movementPath ) ? { movementPath : [ ... mini . movementPath , getCurrentPositionWaypoint ( mini ) ] } : { } ; } , null , 'movementPath' ) ; } export function removeMiniWaypointAction ( miniId ) { return updateMiniAction ( miniId , ( state ) => { const mini = getScenarioFromStore ( state ) . minis [ miniId ] ; return ( mini . movementPath ) ? { movementPath : mini . movementPath . slice ( 0 , mini . movementPath . length - 1 ) } : { } ; } , null , 'movementPath' ) ; } export function cancelMiniMoveAction ( miniId ) { return updateMiniAction ( miniId , ( state ) => { const mini = getScenarioFromStore ( state ) . minis [ miniId ] ; return ( mini . movementPath ) ? { position : mini . movementPath [ 0 ] , elevation : mini . movementPath [ 0 ] . elevation || 0 , movementPath : [ mini . movementPath [ 0 ] ] } : { } ; } , null , 'position+movementPath' ) ; } export function updateMiniVisibilityAction ( miniId , visibility ) { return updateMiniAction ( miniId , { visibility } , null , 'visibility' ) ; } export function updateMiniGMOnlyAction ( miniId , gmOnly ) { return updateMiniAction ( miniId , { gmOnly } , null , 'gmOnly' ) ; } export function updateMiniRosterValueAction ( miniId , column , value ? ) { return updateMiniAction ( miniId , ( state ) => { const mini = getScenarioFromStore ( state ) . minis [ miniId ] ; return column . gmOnly ? { piecesRosterGMValues : { ... mini . piecesRosterGMValues , [ column . id ] : value } } : { piecesRosterValues : { ... mini . piecesRosterValues , [ column . id ] : value } } ; } , null , 'rosterValue' ) ; } export function updateMiniRosterSimpleAction ( miniId , piecesRosterSimple ) { return updateMiniAction ( miniId , { piecesRosterSimple } , null , 'piecesRosterSimple' ) ; } export function updateMiniNoteMarkdownAction ( miniId , gmNoteMarkdown ? ) { return updateMiniAction ( miniId , { gmNoteMarkdown } , null , 'gmNoteMarkdown' ) ; } interface UpdateMinisOnMapActionType { type : ScenarioReducerActionTypes . ADJUST_MINIS_ON_MAP_ACTION ; mapId : string ; gmOnly : boolean ; oldCentre : ObjectVector3 ; newCentre : ObjectVector3 ; deltaPosition ? : THREE . Vector3 ; deltaRotation ? : number ; } function updateMinisOnMapAction ( mapId , gmOnly , oldCentre , newCentre , deltaPosition ? , deltaRotation ? ) { return { type : ScenarioReducerActionTypes . ADJUST_MINIS_ON_MAP_ACTION , mapId , gmOnly , oldCentre , newCentre , deltaPosition , deltaRotation } ; } interface ReplaceMetadataAction extends ScenarioAction { type : ScenarioReducerActionTypes . REPLACE_METADATA_ACTION ; oldMetadataId : string ; newMetadataId : string ; } export function replaceMetadataAction ( oldMetadataId , newMetadataId , gmOnly ) { return populateScenarioActionThunk ( { type : ScenarioReducerActionTypes . REPLACE_METADATA_ACTION , oldMetadataId , newMetadataId , peerKey : 'replaceMetadata' + oldMetadataId , gmOnly } ) ; } interface ReplaceMapImageAction extends ScenarioAction { type : ScenarioReducerActionTypes . REPLACE_MAP_IMAGE_ACTION ; mapId : string ; newMetadataId : string ; gmOnly : boolean ; } export function replaceMapImageAction ( mapId , newMetadataId , gmOnly ) { return populateScenarioActionThunk ( { type : ScenarioReducerActionTypes . REPLACE_MAP_IMAGE_ACTION , mapId , newMetadataId , peerKey : 'replaceMap' + mapId , gmOnly } ) ; } interface UpdateHeadActionIdsAction extends Action { type : ScenarioReducerActionTypes . UPDATE_HEAD_ACTION_IDS ; subtractActionIds : string [ ] ; addActionId : string ; gmOnly : boolean ; } export function updateHeadActionIdsAction ( action ) { return { type : ScenarioReducerActionTypes . UPDATE_HEAD_ACTION_IDS , subtractActionIds : action . headActionIds , addActionId : action . actionId , gmOnly : action . gmOnly } ; } export type ScenarioReducerActionType = UpdateSnapToGridActionType | UpdateConfirmMovesActionType | RemoveMapActionType | UpdateMapActionType | RemoveMiniActionType | UpdateMiniActionType ; function getCurrentPositionWaypoint ( state , updated ? ) { const position = ( updated && updated . position ) || state . position ; const elevation = ( updated && updated . elevation ) || state . elevation ; const onMapId = ( updated && updated . onMapId ) || state . onMapId ; return { ... position , onMapId , elevation } ; } function buildUpdatedHeadActionIds ( headActionIds , subtractActions , addAction ) { const subtracted = headActionIds . filter ( ( actionId ) => ( subtractActions . indexOf ( actionId ) < 0 ) ) ; return subtracted . concat ( addAction ) . sort ( ) ; } const ORIGIN = { x : 0 , y : 0 , z : 0 } ; const ROTATION_NONE = { x : 0 , y : 0 , z : 0 , order : 'XYZ' } ; const initialMapState = { position : ORIGIN , rotation : ROTATION_NONE , gmOnly : true , name : 'New Map' , selectedBy : null , paintLayers : [ ] , transparent : false , metadata : undefined as any } ; function snapToGridReducer ( state = false , action ) { switch ( action . type ) { case ScenarioReducerActionTypes . UPDATE_SNAP_TO_GRID_ACTION : return action . snapToGrid ; default : return state ; } } function confirmMovesReducer ( state = false , action ) { switch ( action . type ) { case ScenarioReducerActionTypes . UPDATE_CONFIRM_MOVES_ACTION : return action . confirmMoves ; default : return state ; } } function singleMapReducer ( state , action ) { switch ( action . type ) { case ScenarioReducerActionTypes . UPDATE_MAP_ACTION : return { ... state , ... action . map } ; default : return state ; } } const allMapsReducer = objectMapReducer < MapType > ( 'mapId' , singleMapReducer as Reducer < MapType > , { deleteActionType : ScenarioReducerActionTypes . REMOVE_MAP_ACTION } ) ; function allMapsFileUpdateReducer ( state = { } , action ) { switch ( action . type ) { case ConnectedUserActionTypes . REMOVE_CONNECTED_USER : return Object . keys ( state ) . reduce < { [ key ] : MapType ; } | undefined > ( ( all , id ) => { if ( state [ id ] . selectedBy === action . peerId ) { all = all || { ... state } ; all [ id ] = { ... state [ id ] , selectedBy : null } ; } return all ; } , undefined ) || state ; case FileIndexActionTypes . UPDATE_FILE_ACTION : const updateFile = action as UpdateFileActionType < void , MapProperties > ; return updateMetadata ( state , updateFile . metadata . id , updateFile . metadata , true , castMapProperties ) ; case ScenarioReducerActionTypes . REPLACE_METADATA_ACTION : const replaceMetadata = action as ReplaceMetadataAction ; return updateMetadata ( state , replaceMetadata . oldMetadataId , { id : replaceMetadata . newMetadataId } , false , castMapProperties ) ; case ScenarioReducerActionTypes . REPLACE_MAP_IMAGE_ACTION : const replaceMapImage = action as ReplaceMapImageAction ; return { ... state , [ replaceMapImage . mapId ] : { ... state [ replaceMapImage . mapId ] , metadata : { id : replaceMapImage . newMetadataId } as any } } ; case FileIndexActionTypes . REMOVE_FILE_ACTION : return removeObjectsReferringToMetadata ( state , action as RemoveFileActionType ) ; default : return allMapsReducer ( state , action ) ; } } function validateMiniState ( fromAction , state ? ) { return ( ( ( state && state . position ) || fromAction . position ) && ( ( state && state . rotation ) || fromAction . rotation ) && ( ( state && state . metadata ) || fromAction . metadata ) && ( ( state && ! isNaN ( state . elevation ) ) || fromAction . elevation !== undefined ) ) || false ; } const singleMiniReducer = ( state , action ) => { switch ( action . type ) { case ScenarioReducerActionTypes . UPDATE_MINI_ACTION : if ( validateMiniState ( action . mini , state ) ) { return { ... state , ... action . mini } ; } else { return state ; } case ScenarioReducerActionTypes . REMOVE_MINI_ACTION : if ( state && state . attachMiniId === action . miniId ) { const remove = action as RemoveMiniActionType ; const position = ( remove . positionObj && remove . rotationObj ) ? vector3ToObject ( buildVector3 ( state . position ) . applyEuler ( buildEuler ( remove . rotationObj ) ) . add ( remove . positionObj as THREE . Vector3 ) ) : state . position ; const rotation = remove . rotationObj ? { x : state . rotation . x + remove . rotationObj . x , y : state . rotation . y + remove . rotationObj . y , z : state . rotation . z + remove . rotationObj . z , order : state . rotation . order } : state . rotation ; const elevation = state . elevation + ( remove . elevation || 0 ) ; return { ... state , attachMiniId : undefined , position , rotation , elevation } ; } else { return state ; } default : return state ; } } ; const allMinisReducer = objectMapReducer < MiniType > ( 'miniId' , singleMiniReducer , { deleteActionType : ScenarioReducerActionTypes . REMOVE_MINI_ACTION , reduceDeleteActionOnAll : true } ) ; function updateAllKeys < T > ( state , action , update ) { return Object . keys ( state ) . reduce < { [ key ] : T ; } | undefined > ( ( all , id ) => { const updatedItem = update ( state [ id ] , action ) ; if ( updatedItem ) { all = all || { ... state } ; all [ id ] = updatedItem ; } return all ; } , undefined ) || state ; } const allMinisBatchUpdateReducer = ( state = { } , action ) => { switch ( action . type ) { case ConnectedUserActionTypes . REMOVE_CONNECTED_USER : return updateAllKeys ( state , action , ( mini , action ) => ( ( mini . selectedBy === action . peerId ) ? { ... mini , selectedBy : null } : undefined ) ) ; case FileIndexActionTypes . UPDATE_FILE_ACTION : const updateFile = action as UpdateFileActionType < void , MiniProperties > ; return updateMetadata ( state , updateFile . metadata . id , updateFile . metadata , true , castMiniProperties ) ; case ScenarioReducerActionTypes . REPLACE_METADATA_ACTION : const replaceMetadata = action as ReplaceMetadataAction ; return updateMetadata ( state , replaceMetadata . oldMetadataId , { id : replaceMetadata . newMetadataId } , false , castMiniProperties ) ; case FileIndexActionTypes . REMOVE_FILE_ACTION : return removeObjectsReferringToMetadata ( state , action as RemoveFileActionType ) ; case ScenarioReducerActionTypes . UPDATE_CONFIRM_MOVES_ACTION : return Object . keys ( state ) . reduce ( ( nextState , miniId ) => { const miniState = state [ miniId ] ; nextState [ miniId ] = { ... miniState , movementPath : action . confirmMoves ? [ getCurrentPositionWaypoint ( miniState ) ] : undefined } ; return nextState ; } , { } ) ; case ScenarioReducerActionTypes . ADJUST_MINIS_ON_MAP_ACTION : return Object . keys ( state ) . reduce < undefined | { [ key ] : MiniType ; } > ( ( nextState , miniId ) => { const miniState = state [ miniId ] ; if ( miniState . onMapId === action . mapId && ( miniState . gmOnly || ! action . gmOnly ) && ! miniState . attachMiniId ) { nextState = nextState || { ... state } ; const position = buildVector3 ( miniState . position ) ; if ( action . deltaRotation ) { position . sub ( action . oldCentre ) . applyEuler ( new THREE . Euler ( 0 , action . deltaRotation , 0 ) ) . add ( action . newCentre ) ; } if ( action . deltaPosition ) { position . add ( action . deltaPosition ) ; } nextState [ miniId ] = { ... miniState , position : vector3ToObject ( position ) , rotation : action . deltaRotation ? { ... miniState . rotation , y : miniState . rotation . y + action . deltaRotation } : miniState . rotation } ; } return nextState ; } , undefined ) || state ; case TabletopReducerActionTypes . UPDATE_TABLETOP_ACTION : const piecesRosterColumns = action . tabletop . piecesRosterColumns ; if ( piecesRosterColumns ) { const playerColumnIds = piecesRosterColumns . filter ( ( column ) => ( ! column . gmOnly ) ) . map ( ( column ) => ( column . id ) ) ; const gmColumnIds = piecesRosterColumns . filter ( ( column ) => ( column . gmOnly ) ) . map ( ( column ) => ( column . id ) ) ; return Object . keys ( state ) . reduce ( ( all , miniId ) => { const combinedValues = { ... state [ miniId ] . piecesRosterValues , ... state [ miniId ] . piecesRosterGMValues } ; all [ miniId ] = { ... state [ miniId ] , piecesRosterValues : pick ( combinedValues , playerColumnIds ) , piecesRosterGMValues : pick ( combinedValues , gmColumnIds ) } ; return all ; } , { } ) ; } return state ; default : return allMinisReducer ( state , action ) ; } } ; const headActionIdReducer = ( state = [ ] , action ) => { if ( action . type === ScenarioReducerActionTypes . UPDATE_HEAD_ACTION_IDS ) { return buildUpdatedHeadActionIds ( state , action . subtractActionIds , action . addActionId ) ; } else { return state ; } } ; const playerHeadActionIdReducer = ( state = [ ] , action ) => { if ( action . type === ScenarioReducerActionTypes . UPDATE_HEAD_ACTION_IDS && ! action . gmOnly ) { return buildUpdatedHeadActionIds ( state , action . subtractActionIds , action . addActionId ) ; } else { return state ; } } ; const scenarioReducer = combineReducers < ScenarioType > ( { snapToGrid : snapToGridReducer , confirmMoves : confirmMovesReducer , startCameraAtOrigin : ( state = false ) => ( state ) , maps : allMapsFileUpdateReducer , minis : allMinisBatchUpdateReducer , headActionIds : headActionIdReducer , playerHeadActionIds : playerHeadActionIdReducer } ) ; export const settableScenarioReducer = ( state , action ) => { switch ( action . type ) { case ScenarioReducerActionTypes . SET_SCENARIO_ACTION : case ScenarioReducerActionTypes . SET_SCENARIO_LOCAL_ACTION : return scenarioReducer ( action . scenario , action ) ; case ScenarioReducerActionTypes . APPEND_SCENARIO_ACTION : return scenarioReducer ( { ... state , maps : { ... ( state && state . maps ) , ... action . scenario . maps } , minis : { ... ( state && state . minis ) , ... action . scenario . minis } } as any , action ) ; case ScenarioReducerActionTypes . UPDATE_MAP_ACTION : if ( ( action . map . position || action . map . rotation ) && state && state . maps [ action . mapId ] ) { const oldMap = state . maps [ action . mapId ] ; const { positionObj : oldPosition , rotationObj : oldRotation } = snapMap ( state . snapToGrid , oldMap . metadata . properties , oldMap . position , oldMap . rotation ) ; const newState = scenarioReducer ( state , action ) ; const newMap = newState . maps [ action . mapId ] ; const { positionObj : newPosition , rotationObj : newRotation } = snapMap ( newState . snapToGrid , newMap . metadata . properties , newMap . position , newMap . rotation ) ; const deltaPosition = action . map . position ? buildVector3 ( newPosition ) . sub ( oldPosition as THREE . Vector3 ) : undefined ; const deltaRotation = action . map . rotation ? newRotation . y - oldRotation . y : undefined ; const { mapDX , mapDZ } = getMapCentreOffsets ( state . snapToGrid , oldMap . metadata . properties ) ; const oldCos = Math . cos ( + oldRotation . y ) ; const oldSin = Math . sin ( + oldRotation . y ) ; const oldCentre = { ... oldPosition , x : oldPosition . x - oldCos * mapDX - oldSin * mapDZ , z : oldPosition . z - oldCos * mapDZ + oldSin * mapDX } ; const newCos = Math . cos ( + newRotation . y ) ; const newSin = Math . sin ( + newRotation . y ) ; const newCentre = { ... newPosition , x : newPosition . x - newCos * mapDX - newSin * mapDZ , z : newPosition . z - newCos * mapDZ + newSin * mapDX } ; const fakeAction = updateMinisOnMapAction ( action . mapId , oldMap . gmOnly , oldCentre , newCentre , deltaPosition , deltaRotation ) ; return scenarioReducer ( scenarioReducer ( state , fakeAction ) , action ) ; } return scenarioReducer ( state , action ) ; default : return scenarioReducer ( state , action ) ; } } ; export default settableScenarioReducer ; interface GetGmOnlyParams { getState : ( ) => ReduxStoreType ; mapId ? : string | null ; miniId ? : string | null ; } function getGmOnly ( { getState , mapId = null , miniId = null } ) { const scenario = getScenarioFromStore ( getState ( ) ) ; if ( mapId ) { return scenario . maps [ mapId ] . gmOnly ; } else if ( miniId ) { return scenario . minis [ miniId ] . gmOnly ; } else { return false ; } } function updateMetadata ( state , metadataId , metadata , merge , convert ) ; function updateMetadata ( state , metadataId , metadata , merge , convert ) ; function updateMetadata < T extends MapType | MiniType > ( state , metadataId , metadata , merge , convert ) { return Object . keys ( state ) . reduce ( ( result , id ) => { if ( state [ id ] . metadata && state [ id ] . metadata . id === metadataId ) { result = result || { ... state } ; result [ id ] = Object . assign ( { } , result [ id ] , { metadata : { ... ( merge && result [ id ] . metadata ) , ... metadata , properties : convert ( metadata . properties ) } } ) ; } return result ; } , undefined ) || state ; } const removeObjectsReferringToMetadata = < T extends MapType | MiniType > ( state , action ) : { [ key ] : T ; } => { return Object . keys ( state ) . reduce ( ( result , id ) => { if ( state [ id ] . metadata && state [ id ] . metadata . id === action . file . id ) { result = result || { ... state } ; delete ( result [ id ] ) ; } return result ; } , undefined ) || state ; } ; export const UNDO_ACTION_TYPE = 'gtove-undo' ; export const REDO_ACTION_TYPE = 'gtove-redo' ; export const SEPARATE_UNDO_GROUP_ACTION_TYPE = 'separate-undo-group-action-type' ; export interface UndoRedoAction extends ScenarioAction { } export function undoAction ( ) { return ( dispatch , getState ) => { const history = getUndoableHistoryFromStore ( getState ( ) ) ; if ( history . past . length > 0 ) { const toScenario = history . past [ history . past . length - 1 ] . scenario ; if ( toScenario ) { dispatch ( populateScenarioAction < UndoRedoAction > ( { type : UNDO_ACTION_TYPE , peerKey : v4 ( ) } , getState ) ) ; const [ , playerScenario ] = scenarioToJson ( toScenario ) ; dispatch ( setScenarioAction ( playerScenario , 'undo' , false , true ) as any ) ; } } } ; } export function redoAction ( ) { return ( dispatch , getState ) => { const history = getUndoableHistoryFromStore ( getState ( ) ) ; if ( history . future . length > 0 ) { const toScenario = history . future [ 0 ] . scenario ; if ( toScenario ) { dispatch ( populateScenarioAction < UndoRedoAction > ( { type : REDO_ACTION_TYPE , peerKey : v4 ( ) } , getState ) ) ; const [ , playerScenario ] = scenarioToJson ( toScenario ) ; dispatch ( setScenarioAction ( playerScenario , 'redo' , false , true ) as any ) ; } } } ; } interface SeparateUndoGroupActionType extends Action { } export function separateUndoGroupAction ( ) { return { type : SEPARATE_UNDO_GROUP_ACTION_TYPE } ; } export function undoGroupAction < A extends Action > ( action , undoGroupId ) { return { ... action , undoGroupId } ; } export function undoGroupThunk < A extends Action > ( thunk , undoGroupId = v4 ( ) ) { return ( baseDispatch , getState ) => { const wrappedDispatch = ( action ) => { if ( typeof ( action ) === 'function' ) { return undoGroupThunk ( action , undoGroupId ) ( baseDispatch as any , getState ) ; } else { baseDispatch ( { ... action , undoGroupId } ) ; return action ; } } ; return thunk ( wrappedDispatch as any , getState ) ; } ; } export function undoGroupActionList < T = Action < string > | GToveThunk < Action < string > > > ( actions , undoGroupId ? ) { return ! undoGroupId ? actions : actions . map ( ( action ) => ( typeof ( action ) === 'function' ? undoGroupThunk ( action , undoGroupId ) : undoGroupAction ( action , undoGroupId ) ) ) ; } export const scenarioUndoGroupBy = ( action , state , history ) => ( action . undoGroupId ? action . undoGroupId : action . peerKey ) ; export const scenarioUndoFilter = ( action ) => { switch ( action . type ) { case TabletopReducerActionTypes . UPDATE_TABLETOP_ACTION : return false ; case ScenarioReducerActionTypes . SET_SCENARIO_LOCAL_ACTION : return true ; default : return isScenarioAction ( action ) && action . peerKey !== undefined ; } } ;