From 05f42247e4fdd42a4257d056e3e959ef1b5b4b10 Mon Sep 17 00:00:00 2001 From: kjirou Date: Sat, 7 Jan 2017 15:44:00 +0900 Subject: [PATCH] Refactor logics of the location --- src/actions/index.js | 14 +++- src/components/SquareMatrix.js | 4 +- src/state-models/complex-apis.js | 28 ++++--- src/state-models/location.js | 99 ++++++++++++++++++------- src/state-models/placement.js | 7 +- src/state-models/unit.js | 6 +- test/state-models/complex-apis.js | 47 +++++++----- test/state-models/location.js | 117 +++++++++++++++++++++++++----- test/state-models/unit.js | 15 ++-- 9 files changed, 244 insertions(+), 93 deletions(-) diff --git a/src/actions/index.js b/src/actions/index.js index 86e87c3..e90dd57 100644 --- a/src/actions/index.js +++ b/src/actions/index.js @@ -2,7 +2,9 @@ const { ACTION_TYPES, BOARD_TYPES, PARAMETERS, STYLES } = require('../immutable/constants'); const { JOB_IDS } = require('../immutable/jobs'); const { computeTick, findOneSquareFromBoardsByPlacement } = require('../state-models/complex-apis'); +const locationMethods = require('../state-models/location'); const { areSamePlacements, isPlacedOnBoard } = require('../state-models/placement'); +const placementMethods = require('../state-models/placement'); const { findSquareByCoordinate, parseMapText } = require('../state-models/square-matrix'); const unitMethods = require('../state-models/unit'); const { createNewUnitCollectionState, findUnitsByPlacement } = require('../state-models/unit-collection'); @@ -193,22 +195,26 @@ const initializeApp = () => { const allies = createNewUnitCollectionState().concat([ Object.assign(unitMethods.createNewAllyState(), { jobId: JOB_IDS.FIGHTER, - placement: { boardType: BOARD_TYPES.SORTIE_BOARD, coordinate: [0, 0] }, + placement: placementMethods.createNewPlacementState(BOARD_TYPES.SORTIE_BOARD, [0, 0]), }), Object.assign(unitMethods.createNewAllyState(), { jobId: JOB_IDS.HEALER, - placement: { boardType: BOARD_TYPES.SORTIE_BOARD, coordinate: [0, 1] }, + placement: placementMethods.createNewPlacementState(BOARD_TYPES.SORTIE_BOARD, [0, 1]), }), Object.assign(unitMethods.createNewAllyState(), { jobId: JOB_IDS.MAGE, - placement: { boardType: BOARD_TYPES.SORTIE_BOARD, coordinate: [1, 3] }, + placement: placementMethods.createNewPlacementState(BOARD_TYPES.SORTIE_BOARD, [1, 3]), }), ]); const enemies = createNewUnitCollectionState().concat([ Object.assign(unitMethods.createNewEnemyState(), { jobId: JOB_IDS.FIGHTER, - destinations: [[0 * 48, 5 * 48], [7 * 48, 5 * 48], [7 * 48, 1 * 48]], + destinations: [ + locationMethods.createNewLocationState(0 * 48, 5 * 48), + locationMethods.createNewLocationState(7 * 48, 5 * 48), + locationMethods.createNewLocationState(7 * 48, 1 * 48), + ], }), ]); diff --git a/src/components/SquareMatrix.js b/src/components/SquareMatrix.js index 9f9ffd6..d0bfecf 100644 --- a/src/components/SquareMatrix.js +++ b/src/components/SquareMatrix.js @@ -46,8 +46,8 @@ const SquareMatrix = ({ squareMatrix, cursorCoordinate, units, unitsOnSquares, h return React.createElement(Unit, { key: 'square-matrix-unit-' + unit.uid, iconId: getIconId(unit), - top: unit.location[0], - left: unit.location[1], + top: unit.location.y, + left: unit.location.x, classNames: [ 'square-matrix__unit', isAlly(unit) ? 'unit--ally' : 'unit--enemy', diff --git a/src/state-models/complex-apis.js b/src/state-models/complex-apis.js index f089a31..741f448 100644 --- a/src/state-models/complex-apis.js +++ b/src/state-models/complex-apis.js @@ -4,7 +4,7 @@ const boxCollide = require('box-collide'); const config = require('../config'); const { ACT_AIM_RANGE_TYPES, BOARD_TYPES, STYLES } = require('../immutable/constants'); -const { expandReachToRelativeCoordinates, matrixAdd } = require('../lib/core'); +const { expandReachToRelativeCoordinates } = require('../lib/core'); const locationMethods = require('./location'); const squareMatrixMethods = require('./square-matrix'); const unitMethods = require('./unit'); @@ -15,7 +15,8 @@ const unitMethods = require('./unit'); * @return {State~Location} A location of square */ const coordinateToSquareLocation = (coordinate) => { - return [coordinate[0] * STYLES.SQUARE_HEIGHT, coordinate[1] * STYLES.SQUARE_WIDTH]; + return locationMethods.createNewLocationState( + coordinate[0] * STYLES.SQUARE_HEIGHT, coordinate[1] * STYLES.SQUARE_WIDTH); }; /** @@ -24,8 +25,8 @@ const coordinateToSquareLocation = (coordinate) => { */ const squareLocationToRect = (squareLocation) => { return { - x: squareLocation[1], - y: squareLocation[0], + x: squareLocation.x, + y: squareLocation.y, width: STYLES.SQUARE_WIDTH, height: STYLES.SQUARE_HEIGHT, }; @@ -59,8 +60,10 @@ const getUnitPositionAsLocation = (unit) => { */ const createReachableRects = (centerSquareLocation, reach) => { return expandReachToRelativeCoordinates(0, reach) - .map(relativeCoordinate => matrixAdd([centerSquareLocation], [coordinateToSquareLocation(relativeCoordinate)])) - .map(([location]) => squareLocationToRect(location)); + .map(relativeCoordinate => + locationMethods.addLocations(centerSquareLocation, coordinateToSquareLocation(relativeCoordinate)) + ) + .map(location => squareLocationToRect(location)); }; /** @@ -135,9 +138,9 @@ const canActorAimActAtTargetedUnit = (actor, act, target) => { /** * @param {State~Unit} actor - * @param {Act} act + * @param {Function} act * @param {State~Unit[]} units - * @return {State~Unit[]} + * @return {?State~Unit} */ const choiceAimedUnit = (actor, act, units) => { const aimableUnits = units @@ -145,6 +148,7 @@ const choiceAimedUnit = (actor, act, units) => { .filter(unit => canActorAimActAtTargetedUnit(actor, act, unit)); const actorLocation = getUnitPositionAsLocation(actor); + aimableUnits.sort((a, b) => { const aLocation = getUnitPositionAsLocation(a); const bLocation = getUnitPositionAsLocation(b); @@ -159,11 +163,11 @@ const choiceAimedUnit = (actor, act, units) => { } // 2nd; Sort by clock-wise - const angleA = angles.fromSlope([-actorLocation[0], actorLocation[1]], [-aLocation[0], aLocation[1]]); - const angleB = angles.fromSlope([-actorLocation[0], actorLocation[1]], [-bLocation[0], bLocation[1]]); - if (angleA < angleB) { + const angleA = locationMethods.measureAngleWithTopAsZero(actorLocation, aLocation); + const angleB = locationMethods.measureAngleWithTopAsZero(actorLocation, bLocation); + if (angleA === null || angleA < angleB) { return -1; - } else if (angleA > angleB) { + } else if (angleB === null || angleA > angleB) { return 1; } diff --git a/src/state-models/location.js b/src/state-models/location.js index 76f8454..b8ff6a4 100644 --- a/src/state-models/location.js +++ b/src/state-models/location.js @@ -1,14 +1,40 @@ -// TODO: `Location` -> `Point` & `[y,x]` -> {x,y} - /** - * @typedef {number[]} State~Location - * @description [y, x] (= [top, left]) position on the battle-board + * @typedef {Object} State~Location + * @description A position on the battle-board + * @property {number} y - A distance from top to bottom in other word. In other words in CSS term is "top". + * @property {number} x - A distance from left to right. In other words in CSS terms is "left". */ /** @module */ +const angles = require('angles'); + + const createNewLocationState = (y, x) => { - return [y, x]; + return { y, x }; +}; + +/** + * @param {...State~Location} locations + * @return {boolean} + */ +const areSameLocations = (...locations) => { + const [first, ...rest] = locations; + return rest.every(v => first.y === v.y && first.x === v.x); +}; + +/** + * @param {...State~Location} locations + * @return {State~Location} + */ +const addLocations = (...locations) => { + const [first, ...rest] = locations; + let { y, x } = first; + rest.forEach(v => { + y += v.y; + x += v.x; + }); + return createNewLocationState(y, x); }; /** @@ -17,44 +43,61 @@ const createNewLocationState = (y, x) => { * @return {number} */ const measureDistance = (a, b) => { - return Math.sqrt(Math.pow(a[0] - b[0], 2) + Math.pow(a[1] - b[1], 2)); + return Math.sqrt(Math.pow(a.y - b.y, 2) + Math.pow(a.x - b.x, 2)); }; /** - * ベクトルの和を行うが、方向は上下左右に限定する。 - * 大量に呼び出されるので処理の速さを優先する。 - * @param {State~Location} initialLocation - * @param {State~Location} terminalLocation - * @param {number} vector - * @return {State~Location} [movedY, movedX] + * Measure the angle of the line with the top as 0 + * @param {State~Location} from + * @param {State~Location} to + * @return {?number} ex) from(0, 0) / to(-1, 0) -> 0 + * from(0, 0) / to(-1, 1) -> 45 + * from(0, 0) / to(0, 1) -> 90 + * from(0, 0) / to(1, 0) -> 180 + * from(0, 0) / to(0, -1) -> 270 + * from(0, 0) / to(0, 0) -> null */ -const performPseudoVectorAddition = (initialLocation, terminalLocation, vector) => { - const [initialY, initialX] = initialLocation; - const [terminalTop, terminalLeft] = terminalLocation; +const measureAngleWithTopAsZero = (from, to) => { + if (areSameLocations(from, to)) return null; + return angles.normalize(angles.fromSlope([from.x, from.y], [to.x, to.y]) - 270); +}; - if (initialY !== terminalTop && initialX !== terminalLeft) { +/** + * ベクトルの和を行うが、方向は上下左右に限定する。 + * 大量に呼び出されるので処理の速さに配慮する。 + * @param {State~Location} initial + * @param {State~Location} terminal + * @param {number} scalar + * @todo Memoize? + * @return {State~Location} { y: movedY, x: movedX } + */ +const performPseudoVectorAddition = (initial, terminal, scalar) => { + if (initial.y !== terminal.y && initial.x !== terminal.x) { throw new Error('It is possible to move only up / down / left / right.'); } - let movedY = initialY; - let movedX = initialX; - - if (initialY < terminalTop) { - movedY = Math.min(terminalTop, initialY + vector); - } else if (initialY > terminalTop) { - movedY = Math.max(terminalTop, initialY - vector); - } else if (initialX < terminalLeft) { - movedX = Math.min(terminalLeft, initialX + vector); - } else if (initialX > terminalLeft) { - movedX = Math.max(terminalLeft, initialX - vector); + let movedY = initial.y; + let movedX = initial.x; + + if (initial.y < terminal.y) { + movedY = Math.min(terminal.y, initial.y + scalar); + } else if (initial.y > terminal.y) { + movedY = Math.max(terminal.y, initial.y - scalar); + } else if (initial.x < terminal.x) { + movedX = Math.min(terminal.x, initial.x + scalar); + } else if (initial.x > terminal.x) { + movedX = Math.max(terminal.x, initial.x - scalar); } - return [movedY, movedX]; + return createNewLocationState(movedY, movedX); }; module.exports = { + addLocations, + areSameLocations, createNewLocationState, + measureAngleWithTopAsZero, measureDistance, performPseudoVectorAddition, }; diff --git a/src/state-models/placement.js b/src/state-models/placement.js index 9518cbd..392993b 100644 --- a/src/state-models/placement.js +++ b/src/state-models/placement.js @@ -9,12 +9,13 @@ const uuidV4 = require('uuid/v4'); const { BOARD_TYPES } = require('../immutable/constants'); +const { createNewCoordinateState } = require('./coordinate'); -const createNewPlacementState = () => { +const createNewPlacementState = (boardType = null, coordinate = null) => { return { - boardType: null, - coordinate: null, + boardType, + coordinate: coordinate === null ? coordinate : createNewCoordinateState(...coordinate), }; }; diff --git a/src/state-models/unit.js b/src/state-models/unit.js index f7355e9..b7a2105 100644 --- a/src/state-models/unit.js +++ b/src/state-models/unit.js @@ -33,7 +33,7 @@ const { FACTION_TYPES, FRIENDSHIP_TYPES, PARAMETERS } = require('../immutable/co const { ACT_IDS, acts } = require('../immutable/acts'); const { JOB_IDS, jobs } = require('../immutable/jobs'); const { createNewPlacementState } = require('./placement'); -const { performPseudoVectorAddition } = require('./location'); +const { areSameLocations, performPseudoVectorAddition } = require('./location'); const createNewUnitState = () => { @@ -146,8 +146,8 @@ const calculateMovementResults = (unit) => { newLocation = currentDestination; } - const newDestinationIndex = unit.destinationIndex + - (newLocation[0] === currentDestination[0] && newLocation[1] === currentDestination[1] ? 1 : 0); + const newDestinationIndex = + unit.destinationIndex + (areSameLocations(newLocation, currentDestination) ? 1 : 0); return { location: newLocation, diff --git a/test/state-models/complex-apis.js b/test/state-models/complex-apis.js index 66b9bb5..8bbc38b 100644 --- a/test/state-models/complex-apis.js +++ b/test/state-models/complex-apis.js @@ -37,9 +37,9 @@ describe('state-models/complex-apis', () => { }); }; - const _createLocatedUnit = (top, left) => { + const _createLocatedUnit = (y, x) => { return Object.assign(unitMethods.createNewUnitState(), { - location: locationMethods.createNewLocationState(top, left), + location: locationMethods.createNewLocationState(y, x), }); }; @@ -61,12 +61,12 @@ describe('state-models/complex-apis', () => { it('can execute correctly', () => { assert.deepStrictEqual( coordinateToSquareLocation(coordinateMethods.createNewCoordinateState(0, 0)), - [0, 0] + locationMethods.createNewLocationState(0, 0) ); assert.deepStrictEqual( coordinateToSquareLocation(coordinateMethods.createNewCoordinateState(1, 2)), - [48, 96] + locationMethods.createNewLocationState(48, 96) ); }); }); @@ -97,21 +97,30 @@ describe('state-models/complex-apis', () => { describe('createReachableRects', () => { it('can execute correctly', () => { - assert.deepStrictEqual(createReachableRects([0, 0], 0), [ - { x: 0, y: 0, width: 48, height: 48 }, - ]); - - assert.deepStrictEqual(createReachableRects([0, 0], 1), [ - { x: 0, y: 0, width: 48, height: 48 }, - { x: 0, y: -48, width: 48, height: 48 }, - { x: 48, y: 0, width: 48, height: 48 }, - { x: 0, y: 48, width: 48, height: 48 }, - { x: -48, y: 0, width: 48, height: 48 }, - ]); - - assert.deepStrictEqual(createReachableRects([100, 150], 0), [ - { x: 150, y: 100, width: 48, height: 48 }, - ]); + assert.deepStrictEqual( + createReachableRects(locationMethods.createNewLocationState(0, 0), 0), + [ + { x: 0, y: 0, width: 48, height: 48 }, + ] + ); + + assert.deepStrictEqual( + createReachableRects(locationMethods.createNewLocationState(0, 0), 1), + [ + { x: 0, y: 0, width: 48, height: 48 }, + { x: 0, y: -48, width: 48, height: 48 }, + { x: 48, y: 0, width: 48, height: 48 }, + { x: 0, y: 48, width: 48, height: 48 }, + { x: -48, y: 0, width: 48, height: 48 }, + ] + ); + + assert.deepStrictEqual( + createReachableRects(locationMethods.createNewLocationState(100, 150), 0), + [ + { x: 150, y: 100, width: 48, height: 48 }, + ] + ); }); }); diff --git a/test/state-models/location.js b/test/state-models/location.js index d5d960f..efbf1cc 100644 --- a/test/state-models/location.js +++ b/test/state-models/location.js @@ -1,13 +1,36 @@ const assert = require('power-assert'); const { + addLocations, + areSameLocations, createNewLocationState, + measureAngleWithTopAsZero, measureDistance, performPseudoVectorAddition, } = require('../../src/state-models/location'); describe('state-models/location', () => { + const _loc = createNewLocationState; + + describe('addLocations', () => { + it('can execute correctly', () => { + assert.deepStrictEqual(addLocations(_loc(0, 0), _loc(0, 0)), _loc(0, 0)); + assert.deepStrictEqual(addLocations(_loc(1, 0), _loc(0, 1)), _loc(1, 1)); + assert.deepStrictEqual(addLocations(_loc(0, 1), _loc(1, 0), _loc(1, 2)), _loc(2, 3)); + assert.deepStrictEqual(addLocations(_loc(10, 10), _loc(-1, -2)), _loc(9, 8)); + }); + }); + + describe('areSameLocations', () => { + it('can execute correctly', () => { + assert.strictEqual(areSameLocations(_loc(0, 0), _loc(0, 0)), true); + assert.strictEqual(areSameLocations(_loc(1, 0), _loc(0, 0)), false); + assert.strictEqual(areSameLocations(_loc(1, 1), _loc(1, 1), _loc(1, 1)), true); + assert.strictEqual(areSameLocations(_loc(1, 1), _loc(1, 1), _loc(1, 2)), false); + }); + }); + describe('measureDistance', () => { it('can execute correctly', () => { const a = createNewLocationState(0, 0); @@ -19,36 +42,96 @@ describe('state-models/location', () => { }); }); + describe('measureAngleWithTopAsZero', () => { + it('can execute correctly', () => { + const times = [ + [[0, 0], [-1, 0], 0], + [[0, 0], [-1, 1], 45], + [[0, 0], [0, 1], 90], + [[0, 0], [1, 1], 135], + [[0, 0], [1, 0], 180], + [[0, 0], [1, -1], 225], + [[0, 0], [0, -1], 270], + [[0, 0], [-1, -1], 315], + ].map(([fromArgs, toArgs, expectedRoundedAngle]) => { + assert.strictEqual( + Math.round( + measureAngleWithTopAsZero(_loc(...fromArgs), _loc(...toArgs)) + ), + expectedRoundedAngle, + `from=[${ fromArgs }], to=[${ toArgs }] -> ${ expectedRoundedAngle }` + ); + }).length; + + assert(times > 0); + }); + + it('should return null if two points are same locations', () => { + assert.strictEqual(measureAngleWithTopAsZero(_loc(0, 0), _loc(0, 0)), null); + assert.strictEqual(measureAngleWithTopAsZero(_loc(1.2, 3.4), _loc(1.2, 3.4)), null); + }); + }); + describe('performPseudoVectorAddition', () => { - it('initialTop < terminaiTop', () => { - assert.deepEqual(performPseudoVectorAddition([1, 0], [3, 0], 1), [2, 0]); - assert.deepEqual(performPseudoVectorAddition([1, 0], [3, 0], 3), [3, 0]); + it('initial.y < terminal.y', () => { + assert.deepEqual( + performPseudoVectorAddition(createNewLocationState(1, 0), createNewLocationState(3, 0), 1), + createNewLocationState(2, 0) + ); + assert.deepEqual( + performPseudoVectorAddition(createNewLocationState(1, 0), createNewLocationState(3, 0), 3), + createNewLocationState(3, 0) + ); }); - it('initialTop > terminaiTop', () => { - assert.deepEqual(performPseudoVectorAddition([3, 0], [1, 0], 1), [2, 0]); - assert.deepEqual(performPseudoVectorAddition([3, 0], [1, 0], 3), [1, 0]); + it('initial.y > terminal.y', () => { + assert.deepEqual( + performPseudoVectorAddition(createNewLocationState(3, 0), createNewLocationState(1, 0), 1), + createNewLocationState(2, 0) + ); + assert.deepEqual( + performPseudoVectorAddition(createNewLocationState(3, 0), createNewLocationState(1, 0), 3), + createNewLocationState(1, 0) + ); }); - it('initialLeft < terminaiLeft', () => { - assert.deepEqual(performPseudoVectorAddition([0, 1], [0, 3], 1), [0, 2]); - assert.deepEqual(performPseudoVectorAddition([0, 1], [0, 3], 3), [0, 3]); + it('initial.x < terminal.x', () => { + assert.deepEqual( + performPseudoVectorAddition(createNewLocationState(0, 1), createNewLocationState(0, 3), 1), + createNewLocationState(0, 2) + ); + assert.deepEqual( + performPseudoVectorAddition(createNewLocationState(0, 1), createNewLocationState(0, 3), 3), + createNewLocationState(0, 3) + ); }); - it('initialLeft > terminaiLeft', () => { - assert.deepEqual(performPseudoVectorAddition([0, 3], [0, 1], 1), [0, 2]); - assert.deepEqual(performPseudoVectorAddition([0, 3], [0, 1], 3), [0, 1]); + it('initial.x > terminal.x', () => { + assert.deepEqual( + performPseudoVectorAddition(createNewLocationState(0, 3), createNewLocationState(0, 1), 1), + createNewLocationState(0, 2) + ); + assert.deepEqual( + performPseudoVectorAddition(createNewLocationState(0, 3), createNewLocationState(0, 1), 3), + createNewLocationState(0, 1) + ); }); - it('initialTop !== terminaiTop && initialLeft !== terminaiLeft', () => { + it('should throw a error if it is `initial.y !== terminai.y && initial.x !== terminai.x`', () => { assert.throws(() => { - performPseudoVectorAddition([1, 2], [3, 4], 1); + performPseudoVectorAddition(createNewLocationState(1, 2), createNewLocationState(3, 4), 1); }, /move only/); }); - it('can perform float type numbers', () => { - assert.deepEqual(performPseudoVectorAddition([1.1, 2.2], [10, 2.2], 1.1), [1.1 + 1.1, 2.2]); - assert.deepEqual(performPseudoVectorAddition([1.1, 2.2], [1.1, 10], 1.1), [1.1, 2.2 + 1.1]); + it('can perform to float type numbers', () => { + assert.deepEqual( + performPseudoVectorAddition(createNewLocationState(1.1, 99.9), createNewLocationState(99.9, 99.9), 2.2), + createNewLocationState(1.1 + 2.2, 99.9) + ); + assert.deepEqual( + performPseudoVectorAddition(createNewLocationState(99.9, 1.1), createNewLocationState(99.9, 99.9), 2.2), + createNewLocationState(99.9, 1.1 + 2.2) + ); }); }); }); diff --git a/test/state-models/unit.js b/test/state-models/unit.js index af6c340..df20361 100644 --- a/test/state-models/unit.js +++ b/test/state-models/unit.js @@ -1,6 +1,7 @@ const assert = require('power-assert'); const { FACTION_TYPES } = require('../../src/immutable/constants'); +const locationMethods = require('../../src/state-models/location'); const { calculateActionPointsRecovery, calculateMovementResults, @@ -28,26 +29,30 @@ describe('state-models/unit', () => { }); it('can move', () => { - unit.destinations = [[1, 2], [2, 2], [2, 3]]; + unit.destinations = [ + locationMethods.createNewLocationState(1, 2), + locationMethods.createNewLocationState(2, 2), + locationMethods.createNewLocationState(2, 3), + ]; unit.movingSpeed = 99; assert.strictEqual(unit.location, null); assert.strictEqual(unit.destinationIndex, 0); Object.assign(unit, calculateMovementResults(unit)); - assert.deepStrictEqual(unit.location, [1, 2]); + assert.deepStrictEqual(unit.location, locationMethods.createNewLocationState(1, 2)); assert.strictEqual(unit.destinationIndex, 1); Object.assign(unit, calculateMovementResults(unit)); - assert.deepStrictEqual(unit.location, [2, 2]); + assert.deepStrictEqual(unit.location, locationMethods.createNewLocationState(2, 2)); assert.strictEqual(unit.destinationIndex, 2); Object.assign(unit, calculateMovementResults(unit)); - assert.deepStrictEqual(unit.location, [2, 3]); + assert.deepStrictEqual(unit.location, locationMethods.createNewLocationState(2, 3)); assert.strictEqual(unit.destinationIndex, 3); Object.assign(unit, calculateMovementResults(unit)); - assert.deepStrictEqual(unit.location, [2, 3]); + assert.deepStrictEqual(unit.location, locationMethods.createNewLocationState(2, 3)); assert.strictEqual(unit.destinationIndex, 3); }); });