diff --git a/cypress/integration/Map-spec.js b/cypress/integration/Map-spec.js
index 76acf18..280f8c6 100644
--- a/cypress/integration/Map-spec.js
+++ b/cypress/integration/Map-spec.js
@@ -48,4 +48,13 @@ describe('Map', () => {
.click()
cy.contains('TRAVEL_PROMPT').should('not.exist')
})
+
+ it('Should be able to instantaneously travel to a planet.', () => {
+ const destinationPlanet = mockState.world.planets.find(
+ planet => planet.name !== mockState.ship.location.name
+ )
+ cy.contains(destinationPlanet.name).click()
+ cy.contains('TRAVEL').click()
+ // ! Assert that the new planet is traveled to
+ })
})
diff --git a/src/components/Map.js b/src/components/Map.js
index c23bd25..282af99 100644
--- a/src/components/Map.js
+++ b/src/components/Map.js
@@ -4,202 +4,103 @@ import { Paper } from '@material-ui/core'
import * as d3 from 'd3'
import { connect } from 'react-redux'
import TravelPrompt from './TravelPrompt'
-
-const radius = 23
-const fill = '#1976d2'
-const fillHover = 'rgb(17, 82, 147)'
-
-const createSvg = (selector, height, width) =>
- d3
- .select(selector)
- .append('svg')
- .attr('height', height)
- .attr('width', width)
-
-const createSimulation = nodes => d3.forceSimulation().nodes(nodes)
-
-const updateLinks = link =>
- link
- .attr('x1', d => d.source.x)
- .attr('y1', d => d.source.y)
- .attr('x2', d => d.target.x)
- .attr('y2', d => d.target.y)
-
-const addForces = (simulation, height, width) =>
- simulation
- .force('charge_force', d3.forceManyBody())
- .force('center_force', d3.forceCenter(width / 2, height / 2))
-
-const createNode = (svg, data) =>
- svg
- .append('g')
- .attr('class', 'nodes')
- .selectAll('circle')
- .data(data)
- .enter()
- .append('g')
- .attr('class', planet =>
- planet.isHomePlanet ? 'node-container home-planet' : 'node-container'
- )
- .attr('style', planet => (planet.isHomePlanet ? 'cursor: default' : ''))
- .attr('id', planet => planet.name)
- .append('circle')
- .attr('r', radius)
- .attr('fill', fill)
-
-const createLink = (svg, data) =>
- svg
- .append('g')
- .attr('class', 'links')
- .selectAll('line')
- .data(data)
- .enter()
- .append('line')
- .attr('stroke-width', 2)
-
-const addEventsToNodes = (svg, setOpen) => {
- svg.selectAll('.node-container').on('mouseover', function(data) {
- // * Function has in scope: data, d3.event, d3.mouse(this), this
-
- if (!data.isHomePlanet) {
- d3.select(this)
- .select('text')
- .style('font-size', '20px')
- d3.select(this)
- .select('circle')
- .attr('fill', fillHover)
- }
- })
- svg.selectAll('.node-container').on('mouseleave', function(data) {
- d3.select(this)
- .select('circle')
- .attr('fill', fill)
- d3.select(this)
- .select('text')
- .style('font-size', '16px')
- })
- svg.selectAll('.node-container').on('click', function(data) {
- if (!data.isHomePlanet) setOpen(true)
- })
-}
-
-const Map = ({ planets, ship }) => {
+import {
+ addEventsToNodes,
+ createHomePlanetInd,
+ createLabels,
+ createLinks,
+ createNodes,
+ createShipInd,
+ createSimulation,
+ createSvg,
+ getHeight,
+ getWidth,
+ updateShipLocation
+} from '../util/map'
+
+/**
+ * TODO - Edit generatePlanets to modify the location to be a random x and random y
+ * TODO - Modify the calculation for centering elements on the circle x. Needs to include calculation for width of element itself
+ */
+const Map = ({ currentShipLocation, planets, ship }) => {
const drawChart = () => {
- const height = d3.select('#map-root').property('clientHeight')
- const width = d3.select('#map-root').property('clientWidth')
+ const height = getHeight('#map-root')
+ const width = getWidth('#map-root')
const svg = createSvg('#map-root', height, width)
- const nodes_data = planets
-
- const simulation = createSimulation(nodes_data)
-
- const handleTick = () => {
- //update circle positions each tick of the simulation
- node.attr('cx', d => d.x).attr('cy', d => d.y)
+ // TODO - Incorporate this into generatePlanets()
+ const planet1Location = { x: -0.25, y: 0 }
+ const planet2Location = { x: 0, y: 0 }
+ const planet3Location = { x: 0.25, y: 0 }
+ const mockPlanetLocations = [
+ planet1Location,
+ planet2Location,
+ planet3Location
+ ]
- textLabels
- .attr('x', ({ x }) => x + radius + 2)
- .attr('y', ({ y }) => y + radius / 2)
+ const nodes_data = planets.map((planet, i) => ({
+ ...planet,
+ location: mockPlanetLocations[i]
+ }))
- homePlanetInd
- .attr('x', ({ x }) => x + radius + 2)
- .attr('y', ({ y }) => y - 10)
+ const links_data = [
+ { source: nodes_data[0].name, target: nodes_data[1].name },
+ { source: nodes_data[1].name, target: nodes_data[2].name }
+ ]
- shipInd.attr('x', ({ x }) => x + radius + 2).attr('y', ({ y }) => y + 30)
+ createSimulation(nodes_data)
- //update link positions
- //simply tells one end of the line to follow one node around
- //and the other end of the line to follow the other node around
- updateLinks(link)
- }
- //add forces
- //we're going to add a charge to each node
- //also going to add a centering force
- addForces(simulation, height, width)
+ createNodes(svg, nodes_data, currentShipLocation, height, width)
- //draw circles for the nodes
- const node = createNode(svg, nodes_data)
+ createLinks(svg, links_data, height, width)
- //add tick instructions:
- simulation.on('tick', handleTick)
+ createLabels(svg, height, width)
- //Time for the links
+ createHomePlanetInd(svg, planets, height, width)
- //Create links data
- const links_data = [
- { source: planets[0].name, target: planets[1].name, distance: 5 },
- { source: planets[1].name, target: planets[2].name, distance: 10 }
- ]
+ createShipInd(svg, ship, height, width)
- //Create the link force
- //We need the id accessor to use named sources and targets
-
- const link_force = d3
- .forceLink(links_data)
- .id(d => d.name)
- .distance(200)
-
- //Add a links force to the simulation
- //Specify links in d3.forceLink argument
-
- simulation.force('links', link_force)
- simulation.force('manyBody', d3.forceManyBody().strength(-800))
-
- //draw lines for the links
- const link = createLink(svg, links_data)
-
- const textLabels = svg
- .selectAll('.node-container')
- .append('text')
- .text(({ name }) => name)
- .attr('x', ({ x }) => x)
- .attr('y', ({ y }) => y)
-
- // Indicate home planet
- const homePlanet = planets.find(planet => planet.isHomePlanet === true)
- const homePlanetInd = svg
- .select(`#${homePlanet.name}`)
- .append('text')
- .text('(Home Planet)')
- .attr('id', 'home-planet-ind')
- .attr('x', ({ x }) => x)
- .attr('y', ({ y }) => y)
- .style('font-size', '10px')
-
- const shipInd = svg
- .select(`#${ship.location.name}`)
- .append('text')
- .text('(YOUR_SHIP)')
- .attr('id', 'ship-ind')
- .attr('x', ({ x }) => x)
- .attr('y', ({ y }) => y)
- .style('font-size', '10px')
-
- addEventsToNodes(svg, setOpen)
+ addEventsToNodes(svg, setDestination, setOpen, currentShipLocation)
}
+ const [open, setOpen] = useState(false)
+ const [destination, setDestination] = useState({})
+
useEffect(() => {
drawChart()
// eslint-disable-next-line
}, [])
- const [open, setOpen] = useState(false)
+ useEffect(() => {
+ updateShipLocation(
+ currentShipLocation,
+ setOpen,
+ setDestination,
+ d3.select('#map-root').property('clientHeight'),
+ d3.select('#map-root').property('clientWidth')
+ )
+ }, [currentShipLocation])
return (
-
+
)
}
Map.propTypes = {
+ currentShipLocation: PropTypes.object.isRequired,
planets: PropTypes.array.isRequired,
ship: PropTypes.object.isRequired
}
-const mapStateToProps = ({ ship, world }) => ({ planets: world.planets, ship })
+const mapStateToProps = ({ ship, world }) => ({
+ currentShipLocation: ship.location,
+ planets: world.planets,
+ ship
+})
const mapDispatchToProps = dispatch => ({})
diff --git a/src/components/MarketTable.js b/src/components/MarketTable.js
index 3246d47..1ed0125 100644
--- a/src/components/MarketTable.js
+++ b/src/components/MarketTable.js
@@ -9,7 +9,7 @@ import {
TableSortLabel
} from '@material-ui/core'
import ItemCard from '../components/ItemCard'
-import { simpleCompare } from '../util'
+import { simpleCompare } from '../util/main'
import MarketAvatar from './MarketAvatar'
const MarketTable = ({ data, item }) => {
diff --git a/src/components/PlanetDisplay.js b/src/components/PlanetDisplay.js
index 7329a1c..3a7dca1 100644
--- a/src/components/PlanetDisplay.js
+++ b/src/components/PlanetDisplay.js
@@ -4,7 +4,7 @@ import { connect } from 'react-redux'
import { Box, Button, Heading, Text } from 'grommet'
import { Target } from 'grommet-icons'
import { departShip } from '../redux/actions/ship'
-import { createETA, createDiffDuration } from '../util'
+import { createETA, createDiffDuration } from '../util/main'
/**
* Displays planet statistics.
diff --git a/src/components/Sidebar.js b/src/components/Sidebar.js
index 9abce7f..0673a71 100644
--- a/src/components/Sidebar.js
+++ b/src/components/Sidebar.js
@@ -8,7 +8,7 @@ import { views } from '../constants'
import Title from './Title'
import TravelTimer from './TravelTimer'
import CashDisplay from './CashDisplay'
-import { exportGame } from '../util'
+import { exportGame } from '../util/main'
import ImportButton from './ImportButton'
import { Tag } from 'flwww'
diff --git a/src/components/TravelPrompt.js b/src/components/TravelPrompt.js
index 8d0d6b6..9a71043 100644
--- a/src/components/TravelPrompt.js
+++ b/src/components/TravelPrompt.js
@@ -1,12 +1,17 @@
import React from 'react'
+import PropTypes from 'prop-types'
import {
+ Button,
Dialog,
+ DialogActions,
DialogContent,
DialogContentText,
DialogTitle
} from '@material-ui/core'
+import { connect } from 'react-redux'
+import { instantTravel } from '../redux/actions/ship'
-const TravelPrompt = ({ open, setOpen }) => {
+const TravelPrompt = ({ destination, handleTravel, open, setOpen }) => {
const handleClose = () => setOpen(false)
return (
)
}
-export default TravelPrompt
+TravelPrompt.propTypes = {
+ destination: PropTypes.object.isRequired,
+ handleTravel: PropTypes.func.isRequired,
+ open: PropTypes.bool.isRequired,
+ setOpen: PropTypes.func.isRequired
+}
+
+const mapStateToProps = () => ({})
+
+const mapDispatchToProps = dispatch => ({
+ handleTravel: (destination, setOpen) => {
+ dispatch(instantTravel(destination))
+ setOpen(false)
+ }
+})
+
+export default connect(
+ mapStateToProps,
+ mapDispatchToProps
+)(TravelPrompt)
diff --git a/src/redux/actions/ship.js b/src/redux/actions/ship.js
index 9551da9..c2ccfa5 100644
--- a/src/redux/actions/ship.js
+++ b/src/redux/actions/ship.js
@@ -1,6 +1,6 @@
import { addCash, removeCash } from './user'
import { removeItem } from './world'
-import { createETA, createDiffDuration } from '../../util'
+import { createETA, createDiffDuration } from '../../util/main'
import moment from 'moment'
// * ACTION TYPES
@@ -188,3 +188,7 @@ export const purchaseCargo = (item, quantity) => dispatch => {
// * dispatch an action to remove the item from the list of stored items on this planet
dispatch(removeItem(item, quantity))
}
+
+export const instantTravel = destination => dispatch => {
+ dispatch(setShipLocation({ name: destination.name }))
+}
diff --git a/src/redux/actions/world.js b/src/redux/actions/world.js
index 71f05e2..bba5681 100644
--- a/src/redux/actions/world.js
+++ b/src/redux/actions/world.js
@@ -1,5 +1,5 @@
import { setShipLocation } from './ship'
-import { generateBuyers, generatePlanets, generateSellers } from '../../util'
+import { generateBuyers, generatePlanets, generateSellers } from '../../util/main'
import { setBuyers, setSellers } from './market'
// * ACTION TYPES
diff --git a/src/redux/reducers/world.js b/src/redux/reducers/world.js
index 76ece29..9b2ec2b 100644
--- a/src/redux/reducers/world.js
+++ b/src/redux/reducers/world.js
@@ -1,4 +1,4 @@
-import { generateItems } from '../../util'
+import { generateItems } from '../../util/main'
const worldDefaultState = {
isTimerRunning: false,
diff --git a/src/redux/store/store.js b/src/redux/store/store.js
index 1eb0453..6205a74 100644
--- a/src/redux/store/store.js
+++ b/src/redux/store/store.js
@@ -5,7 +5,7 @@ import shipReducer from '../reducers/ship'
import uiReducer from '../reducers/ui'
import userReducer from '../reducers/user'
import worldReducer from '../reducers/world'
-import { loadState, saveState } from '../../util'
+import { loadState, saveState } from '../../util/main'
import throttle from 'lodash/throttle'
const composeEnhancer = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose
diff --git a/src/util.js b/src/util/main.js
similarity index 99%
rename from src/util.js
rename to src/util/main.js
index 1d40842..562b567 100644
--- a/src/util.js
+++ b/src/util/main.js
@@ -1,4 +1,4 @@
-import { itemList, planets, firstNames, lastNames, colors } from './constants'
+import { itemList, planets, firstNames, lastNames, colors } from '../constants'
import uuidv4 from 'uuid/v4'
import moment from 'moment'
import { saveAs } from 'file-saver'
diff --git a/src/util/map.js b/src/util/map.js
new file mode 100644
index 0000000..5616b92
--- /dev/null
+++ b/src/util/map.js
@@ -0,0 +1,188 @@
+import * as d3 from 'd3'
+
+const radius = 23
+const fill = '#1976d2'
+const fillHover = 'rgb(17, 82, 147)'
+
+export const createSvg = (selector, height, width) =>
+ d3
+ .select(selector)
+ .append('svg')
+ .attr('height', height)
+ .attr('width', width)
+
+export const createSimulation = data =>
+ d3
+ .forceSimulation()
+ .nodes(data)
+ .stop()
+
+export const modifyCursor = (selection, currentShipLocation) =>
+ selection.attr('style', ({ name }) =>
+ name !== currentShipLocation.name ? 'cursor: pointer' : 'cursor: default'
+ )
+
+const createCircle = (selection, height, width) =>
+ selection
+ .append('circle')
+ .attr('r', radius)
+ .attr('fill', fill)
+ .attr('cx', ({ location }) => width / 2 + location.x * width)
+ .attr('cy', ({ location }) => height / 2 - location.y * height)
+
+export const createNodes = (svg, data, currentShipLocation, height, width) => {
+ const selection = svg
+ .append('g')
+ .attr('class', 'nodes')
+ .selectAll('circle')
+ .data(data)
+ .enter()
+ .append('g')
+ .attr('class', 'node-container')
+ .attr('id', ({ name }) => name)
+
+ modifyCursor(selection, currentShipLocation)
+ createCircle(selection, height, width)
+
+ return selection
+}
+
+export const createLinks = (svg, data, height, width) =>
+ svg
+ .append('g')
+ .attr('class', 'links')
+ .selectAll('line')
+ .data(data)
+ .enter()
+ .append('line')
+ .attr('stroke-width', 2)
+ .attr(
+ 'x1',
+ ({ source }) =>
+ width / 2 +
+ svg.select(`#${source}`).data()[0].location.x * width +
+ radius
+ )
+ .attr(
+ 'y1',
+ ({ source }) =>
+ height / 2 + svg.select(`#${source}`).data()[0].location.y * height
+ )
+ .attr(
+ 'x2',
+ ({ target }) =>
+ width / 2 +
+ svg.select(`#${target}`).data()[0].location.x * width -
+ radius
+ )
+ .attr(
+ 'y2',
+ ({ target }) =>
+ height / 2 + svg.select(`#${target}`).data()[0].location.y * height
+ )
+
+export const createLabels = (svg, height, width) =>
+ svg
+ .selectAll('.node-container')
+ .append('text')
+ .text(({ name }) => name)
+ .attr('x', ({ location }) => width / 2 + location.x * width - radius)
+ .attr('y', ({ location }) => height / 2 + location.y * height + radius + 20)
+
+export const createHomePlanetInd = (svg, planets, height, width) => {
+ const homePlanet = planets.find(planet => planet.isHomePlanet === true)
+
+ const selection = svg
+ .select(`#${homePlanet.name}`)
+ .append('text')
+ .text('(Home Planet)')
+ .attr('id', 'home-planet-ind')
+ .style('font-size', '10px')
+ .attr('x', ({ location }) => width / 2 + location.x * width - radius - 4)
+ .attr('y', ({ location }) => height / 2 + location.y * height + radius + 40)
+
+ return selection
+}
+
+export const createShipInd = (svg, ship, height, width) =>
+ svg
+ .select(`#${ship.location.name}`)
+ .append('text')
+ .text('(YOUR_SHIP)')
+ .attr('id', 'ship-ind')
+ .style('font-size', '10px')
+ .attr('x', ({ location }) => width / 2 + location.x * width - radius - 8)
+ .attr('y', ({ location }) => height / 2 + location.y * height - radius - 10)
+
+export const addEventsToNodes = (
+ svg,
+ setDestination,
+ setOpen,
+ currentShipLocation
+) => {
+ svg.selectAll('.node-container').on('mouseover', function(planet) {
+ // * Function has in scope: data, d3.event, d3.mouse(this), this
+
+ if (planet.name !== currentShipLocation.name) {
+ d3.select(this)
+ .select('text')
+ .style('font-size', '20px')
+ d3.select(this)
+ .select('circle')
+ .attr('fill', fillHover)
+ }
+ })
+ svg.selectAll('.node-container').on('mouseleave', function(planet) {
+ d3.select(this)
+ .select('circle')
+ .attr('fill', fill)
+ d3.select(this)
+ .select('text')
+ .style('font-size', '16px')
+ })
+ svg.selectAll('.node-container').on('click', function(planet) {
+ if (planet.name !== currentShipLocation.name) {
+ setDestination(planet)
+ setOpen(true)
+ }
+ })
+}
+
+export const updateShipLocation = (
+ currentShipLocation,
+ setOpen,
+ setDestination,
+ height,
+ width
+) => {
+ const svg = d3.select('svg')
+ const destinationNode = svg.select(`#${currentShipLocation.name}`)
+
+ svg
+ .select(`#ship-ind`)
+ .attr(
+ 'x',
+ () =>
+ width / 2 + destinationNode.data()[0].location.x * width - radius - 8
+ )
+ .attr(
+ 'y',
+ () =>
+ height / 2 + destinationNode.data()[0].location.y * height - radius - 10
+ )
+
+ // * Remove event listeners from nodes
+ svg.selectAll('.node-container').on('mouseover', null)
+ svg.selectAll('.node-container').on('mouseleave', null)
+ svg.selectAll('.node-container').on('click', null)
+
+ addEventsToNodes(svg, setDestination, setOpen, currentShipLocation)
+
+ // * Update cursor stuff
+ modifyCursor(svg.selectAll('.node-container'), currentShipLocation)
+}
+
+export const getHeight = selector =>
+ d3.select(selector).property('clientHeight')
+
+export const getWidth = selector => d3.select(selector).property('clientWidth')