Skip to content

Commit

Permalink
✨ Instant Travel
Browse files Browse the repository at this point in the history
  • Loading branch information
alexlee-dev committed Sep 22, 2019
1 parent d31b3d7 commit 1de2e3b
Show file tree
Hide file tree
Showing 12 changed files with 310 additions and 173 deletions.
9 changes: 9 additions & 0 deletions cypress/integration/Map-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
})
})
227 changes: 64 additions & 163 deletions src/components/Map.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<Fragment>
<Paper id="map-root" style={{ height: 'calc(100vh - 50px)' }} />
<TravelPrompt open={open} setOpen={setOpen} />
<TravelPrompt destination={destination} open={open} setOpen={setOpen} />
</Fragment>
)
}

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 => ({})

Expand Down
2 changes: 1 addition & 1 deletion src/components/MarketTable.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 }) => {
Expand Down
2 changes: 1 addition & 1 deletion src/components/PlanetDisplay.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion src/components/Sidebar.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down
39 changes: 37 additions & 2 deletions src/components/TravelPrompt.js
Original file line number Diff line number Diff line change
@@ -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 (
<Dialog open={open} onClose={handleClose}>
Expand All @@ -16,8 +21,38 @@ const TravelPrompt = ({ open, setOpen }) => {
Are you sure you want to travel to PLANET_NAME?
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={handleClose} color="secondary">
Cancel
</Button>
<Button
onClick={() => handleTravel(destination, setOpen)}
color="primary"
>
TRAVEL
</Button>
</DialogActions>
</Dialog>
)
}

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)
6 changes: 5 additions & 1 deletion src/redux/actions/ship.js
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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 }))
}
2 changes: 1 addition & 1 deletion src/redux/actions/world.js
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/redux/reducers/world.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { generateItems } from '../../util'
import { generateItems } from '../../util/main'

const worldDefaultState = {
isTimerRunning: false,
Expand Down
Loading

0 comments on commit 1de2e3b

Please sign in to comment.