-
Notifications
You must be signed in to change notification settings - Fork 0
/
complex-apis.js
259 lines (219 loc) · 7.47 KB
/
complex-apis.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
/** @module */
const angles = require('angles');
const boxCollide = require('box-collide');
const config = require('../config');
const { ACT_AIM_RANGE_TYPES, BOARD_TYPES, STYLES } = require('../immutable/constants');
const { expandReachToRelativeCoordinates } = require('../lib/core');
const locationMethods = require('./location');
const squareMatrixMethods = require('./square-matrix');
const unitMethods = require('./unit');
/**
* @param {State~Coordinate}
* @return {State~Location} A location of square
*/
const coordinateToSquareLocation = (coordinate) => {
return locationMethods.createNewLocationState(
coordinate[0] * STYLES.SQUARE_HEIGHT, coordinate[1] * STYLES.SQUARE_WIDTH);
};
/**
* @param {State~Location} squareLocation
* @return {{x, y, width, height}} This is mainly used to `substack/box-collide`
*/
const squareLocationToRect = (squareLocation) => {
return {
x: squareLocation.x,
y: squareLocation.y,
width: STYLES.SQUARE_WIDTH,
height: STYLES.SQUARE_HEIGHT,
};
};
/**
* @param {State~Coordinate} coordinate
*/
const coordinateToRect = (coordinate) => {
return squareLocationToRect(coordinateToSquareLocation(coordinate));
};
/**
* @param {State~Unit} unit
* @return {State~Location}
*/
const getUnitPositionAsLocation = (unit) => {
if (unit.location) {
return unit.location;
} else if (unit.placement) {
return coordinateToSquareLocation(unit.placement.coordinate);
}
throw new Error(`The unit does not have either \`location\` or \`placement\``);
};
/**
* @param {State~Location} centerSquareLocation
* @param {number} reach - A integer >= 0
* @return {Array<{x, y, width, height}>}
* @todo Remove overflowed coordinates from result?
*/
const createReachableRects = (centerSquareLocation, reach) => {
return expandReachToRelativeCoordinates(0, reach)
.map(relativeCoordinate =>
locationMethods.addLocations(centerSquareLocation, coordinateToSquareLocation(relativeCoordinate))
)
.map(location => squareLocationToRect(location));
};
/**
* @return {State~Square[]}
*/
const findSquaresFromBoardsByPlacement = (placement, ...boards) => {
const squares = [];
boards
.filter(board => placement.boardType === board.boardType)
.forEach(board => {
const square = squareMatrixMethods.findSquareByCoordinate(board.squareMatrix, placement.coordinate);
if (square) squares.push(square);
})
;
return squares;
};
/**
* @param {State~Placement} placement
* @param {...State~Board} boards
* @throws {Error} Found multiple squares
* @return {?State~Square}
*/
const findOneSquareFromBoardsByPlacement = (placement, ...boards) => {
const squares = findSquaresFromBoardsByPlacement(placement, ...boards);
if (squares.length > 1) {
throw new Error(`There are multiple squares found by the placement of ${ JSON.stringify(placement) }`);
}
return squares[0] || null;
};
/**
* @param {State~Unit} unit
* @return {boolean}
*/
const isUnitInBattle = (unit) => {
return unit.placement.boardType === BOARD_TYPES.BATTLE_BOARD && unit.hp > 0;
};
/**
* @param {State~Unit} actor - Only those in battle
* @param {Act} act
* @param {State~Unit} unit - Only those in battle
* @return {boolean}
*/
const willActorAimActAtUnit = (actor, act, unit) => {
return act.friendshipType === unitMethods.determineFriendship(actor, unit);
};
/**
* @param {State~Unit} actor - Only those in battle
* @param {Act} act
* @param {State~Unit} target - Only those in battle
* @return {boolean}
*/
const canActorAimActAtTargetedUnit = (actor, act, target) => {
if (act.aimRange.type === ACT_AIM_RANGE_TYPES.REACHABLE) {
// TODO: 暗黙的にユニットのサイズをマス目と同じとしているが、これで問題ないのか不明。
const actorLocation = getUnitPositionAsLocation(actor);
const reachableRects = createReachableRects(actorLocation, act.aimRange.reach);
const targetLocation = getUnitPositionAsLocation(target);
const targetRect = squareLocationToRect(targetLocation);
return reachableRects.some(rect => boxCollide(rect, targetRect));
}
throw new Error(`Invalid aim-range-type`);
};
/**
* @param {State~Unit} actor
* @param {Function} act
* @param {State~Unit[]} units
* @return {?State~Unit}
*/
const choiceAimedUnit = (actor, act, units) => {
const aimableUnits = units
.filter(unit => willActorAimActAtUnit(actor, act, unit))
.filter(unit => canActorAimActAtTargetedUnit(actor, act, unit));
const actorLocation = getUnitPositionAsLocation(actor);
aimableUnits.sort((a, b) => {
const aLocation = getUnitPositionAsLocation(a);
const bLocation = getUnitPositionAsLocation(b);
// 1st: Sort by closest
const actorToA = locationMethods.measureDistance(actorLocation, aLocation);
const actorToB = locationMethods.measureDistance(actorLocation, bLocation);
if (actorToA < actorToB) {
return -1;
} else if (actorToA > actorToB) {
return 1;
}
// 2nd; Sort by clock-wise
const angleA = locationMethods.measureAngleWithTopAsZero(actorLocation, aLocation);
const angleB = locationMethods.measureAngleWithTopAsZero(actorLocation, bLocation);
if (angleA === null || angleA < angleB) {
return -1;
} else if (angleB === null || angleA > angleB) {
return 1;
}
return 0;
});
return aimableUnits[0] || null;
};
/**
* Compute the state transition during the "tick"
* <section>
* The "tick" is a coined word, which means so-called "one game loop".
* </section>
* @param {Object} state - A plain object generated from `store.getState()`
* @return {Object}
*/
const computeTick = ({ allies, enemies, gameStatus }) => {
let newAllies = allies.slice();
let newEnemies = enemies.slice();
// TODO: 死亡者を盤上から除去するのは、ループの最初と最後どこで行う?または両方で?
// 想定ケース)
// - 味方の手動退却 || 味方の死亡 -> 盤上から取り除かれてHP回復/出撃ゲージ空
// - 既に前の味方の攻撃で倒している敵を攻撃してしまう
// Ally's act
newAllies = newAllies.map(ally => {
const newAlly = Object.assign({}, ally);
if (!isUnitInBattle(newAlly)) {
// TODO: 出撃ポイントの回復
return newAlly;
}
const act = unitMethods.getAct(newAlly);
let didAct = false;
if (unitMethods.canDoAct(newAlly)) {
const aimedUnit = choiceAimedUnit(newAlly, act, newAllies.concat(newEnemies));
if (aimedUnit) {
if (config.isEnabledTickLog) {
console.debug(`${ newAlly.factionType }:${ newAlly.jobId } aims ${ act.id } at ${ aimedUnit.factionType }:${ aimedUnit.jobId }`);
}
// TODO: 効果を発生させる
// Comsume AP
newAlly.actionPoints = unitMethods.calculateActionPointsConsumption(newAlly, act);
didAct = true;
}
}
if (!didAct) {
// Recover AP
newAlly.actionPoints = unitMethods.calculateActionPointsRecovery(newAlly);
}
return newAlly;
});
// Enemy's act or movement
newEnemies = newEnemies.map(enemy => {
const { location, destinationIndex } = unitMethods.calculateMovementResults(enemy);
return Object.assign({}, enemy, {
location,
destinationIndex,
});
});
return {
allies: newAllies,
enemies: newEnemies,
};
};
module.exports = {
canActorAimActAtTargetedUnit,
choiceAimedUnit,
computeTick,
coordinateToSquareLocation,
coordinateToRect,
createReachableRects,
findOneSquareFromBoardsByPlacement,
willActorAimActAtUnit,
};