@thegraid/hexlib 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/hex.js ADDED
@@ -0,0 +1,714 @@
1
+ import { C, F } from "@thegraid/easeljs-lib";
2
+ import { Container, Point, Text } from "@thegraid/easeljs-module";
3
+ import { H } from "./hex-intfs";
4
+ import { HexShape, LegalMark } from "./shapes";
5
+ import { TP } from "./table-params";
6
+ export const S_Resign = 'Hex@Resign';
7
+ export const S_Skip = 'Hex@skip ';
8
+ export function newHSC(hex, sc, Aname = hex.Aname) { return { Aname, hex, sc }; }
9
+ /** to recognize this class in hexUnderPoint and obtain the associated Hex2. */
10
+ class HexCont extends Container {
11
+ hex2;
12
+ constructor(hex2) {
13
+ super();
14
+ this.hex2 = hex2;
15
+ }
16
+ }
17
+ /** Base Hex, has no connection to graphics.
18
+ * topological links to adjacent hex objects.
19
+ */
20
+ export class Hex {
21
+ /** return indicated Hex from otherMap */
22
+ static ofMap(ihex, otherMap) {
23
+ try {
24
+ return otherMap[ihex.row][ihex.col];
25
+ }
26
+ catch (err) {
27
+ console.warn(`ofMap failed:`, err, { ihex, otherMap }); // eg: otherMap is different (mh,nh)
28
+ throw err;
29
+ }
30
+ }
31
+ static aname(row, col) {
32
+ return (row >= 0) ? `Hex@[${row},${col}]` : col == -1 ? S_Skip : S_Resign;
33
+ }
34
+ constructor(map, row, col, name = Hex.aname(row, col)) {
35
+ this.Aname = name;
36
+ this.map = map;
37
+ this.row = row;
38
+ this.col = col;
39
+ this.links = {};
40
+ }
41
+ /** (x,y): center of hex; (width,height) of hex; scaled by radius if supplied
42
+ * @param radius [1] radius used in drawPolyStar(radius,,, H.dirRot[tiltDir])
43
+ * @param ewTopo [TP.useEwTopo] true -> suitable for ewTopo (long axis of hex is N/S)
44
+ * @param row [this.row]
45
+ * @param col [this.col]
46
+ * @returns \{ x, y, w, h, dxdc, dydr } of cell at [row, col]
47
+ */
48
+ xywh(radius = TP.hexRad, ewTopo = TP.useEwTopo, row = this.row, col = this.col) {
49
+ if (ewTopo) { // tiltDir = 'NE'; tilt = 30-degrees; nsTOPO
50
+ const h = 2 * radius, w = radius * H.sqrt3; // h height of hexagon (long-vertical axis)
51
+ const dxdc = w;
52
+ const dydr = 1.5 * radius;
53
+ const x = (col + Math.abs(Math.floor(row) % 2) / 2) * dxdc;
54
+ const y = (row) * dydr; // dist between rows
55
+ return { x, y, w, h, dxdc, dydr };
56
+ }
57
+ else { // tiltdir == 'N'; tile = 0-degrees; ewTOPO
58
+ const w = 2 * radius, h = radius * H.sqrt3; // radius * 1.732
59
+ const dxdc = 1.5 * radius;
60
+ const dydr = h;
61
+ const x = (col) * dxdc;
62
+ const y = (row + Math.abs(Math.floor(col) % 2) / 2) * dydr;
63
+ return { x, y, w, h, dxdc, dydr };
64
+ }
65
+ }
66
+ get xywh0() { return this.xywh(); } // so can see xywh from debugger
67
+ Aname;
68
+ /** reduce to serializable IHex (removes map, inf, links, etc) */
69
+ get iHex() { return { Aname: this.Aname, row: this.row, col: this.col }; }
70
+ nf(n) { return `${n !== undefined ? (n === Math.floor(n)) ? n : n.toFixed(1) : ''}`; }
71
+ /** [row,col] OR special name */
72
+ get rcs() { return (this.row >= 0) ? `[${this.nf(this.row)},${this.nf(this.col)}]` : this.Aname.substring(4); }
73
+ get rowsp() { return (this.nf(this.row ?? -1)).padStart(2); }
74
+ get colsp() { return (this.nf(this.col ?? -1)).padStart(2); } // col== -1 ? S_Skip; -2 ? S_Resign
75
+ /** [row,col] OR special name */
76
+ get rcsp() { return (this.row >= 0) ? `[${this.rowsp},${this.colsp}]` : this.Aname.substring(4).padEnd(7); }
77
+ /** compute ONCE, *after* HexMap is populated with all the Hex! */
78
+ get rc_linear() { return this._rcLinear || (this._rcLinear = this.map.rcLinear(this.row, this.col)); }
79
+ _rcLinear = undefined;
80
+ /** accessor so Hex2 can override-advise */
81
+ _district; // district ID
82
+ get district() { return this._district; }
83
+ set district(d) {
84
+ this._district = d;
85
+ }
86
+ get isOnMap() { return this.district !== undefined; } // also: (row !== undefined) && (col !== undefined)
87
+ _isLegal;
88
+ get isLegal() { return this._isLegal; }
89
+ set isLegal(v) { this._isLegal = v; }
90
+ map; // Note: this.parent == this.map.hexCont [cached] TODO: typify ??
91
+ row;
92
+ col;
93
+ /** Link to neighbor in each H.dirs direction [NE, E, SE, SW, W, NW] */
94
+ links = {};
95
+ get linkDirs() { return Object.keys(this.links); }
96
+ /** colorScheme(playerColor)@rcs */
97
+ toString() {
98
+ return `Hex@${this.rcs}`; // hex.toString => Hex@[r,c] | Hex@Skip , Hex@Resign
99
+ }
100
+ /** hex.rcspString => Hex@[ r, c] | 'Hex@Skip ' , 'Hex@Resign ' */
101
+ rcspString() {
102
+ return `Hex@${this.rcsp}`;
103
+ }
104
+ /** convert LINKS object to Array of Hex */
105
+ get linkHexes() {
106
+ return Object.keys(this.links).map((dir) => this.links[dir]);
107
+ }
108
+ forEachLinkHex(func, inclCenter = false) {
109
+ if (inclCenter)
110
+ func(this, undefined, this);
111
+ this.linkDirs.forEach((dir) => func(this.links[dir], dir, this));
112
+ }
113
+ /** return HexDir to the first linked hex that satisfies predicate. */
114
+ findLinkHex(pred) {
115
+ return this.linkDirs.find((dir) => pred(this.links[dir], dir, this));
116
+ }
117
+ /** continue in HexDir until pred is satisfied. */
118
+ findInDir(dir, pred) {
119
+ let hex = this;
120
+ do {
121
+ if (pred(hex, dir, this))
122
+ return hex;
123
+ } while (!!(hex = hex.nextHex(dir)));
124
+ return undefined;
125
+ }
126
+ /** array of all hexes in line from dir. */
127
+ hexesInDir(dir, rv = []) {
128
+ let hex = this;
129
+ while (!!(hex = hex.links[dir]))
130
+ rv.push(hex);
131
+ return rv;
132
+ }
133
+ /** for each Hex in each Dir: func(hex, dir, this) */
134
+ forEachHexDir(func) {
135
+ this.linkDirs.forEach((dir) => this.hexesInDir(dir).filter(hex => !!hex).map(hex => func(hex, dir, this)));
136
+ }
137
+ nextHex(ds, ns = 1) {
138
+ let hex = this;
139
+ while (!!(hex = hex.links[ds]) && --ns > 0) { }
140
+ return hex;
141
+ }
142
+ /** return last Hex on axis in given direction */
143
+ lastHex(ds) {
144
+ let hex = this, nhex;
145
+ while (!!(nhex = hex.links[ds])) {
146
+ hex = nhex;
147
+ }
148
+ return hex;
149
+ }
150
+ /** distance between Hexes: adjacent = 1, based on row, col, sqrt3 */
151
+ radialDist(hex) {
152
+ let unit = 1 / H.sqrt3; // so w = delta(col) = 1
153
+ let { x: tx, y: ty } = this.xywh(unit), { x: hx, y: hy } = hex.xywh(unit);
154
+ let dx = tx - hx, dy = ty - hy;
155
+ return Math.sqrt(dx * dx + dy * dy);
156
+ }
157
+ }
158
+ /**
159
+ * Hex1 may be occupied by [tile?: MapTile, meep?: Meeple].
160
+ */
161
+ export class Hex1 extends Hex {
162
+ _tile;
163
+ get tile() { return this._tile; }
164
+ set tile(tile) { this._tile = tile; } // override in Hex2!
165
+ // Note: set hex.tile mostly invoked from: tile.hex = hex;
166
+ _meep;
167
+ get meep() { return this._meep; }
168
+ set meep(meep) { this._meep = meep; }
169
+ get occupied() { return (this.tile || this.meep) ? [this.tile, this.meep] : undefined; }
170
+ /** colorScheme(playerColor)@rcs */
171
+ toString(sc = this.tile?.player?.color || this.meep?.player?.color) {
172
+ return `${sc ?? 'Empty'}@${this.rcs}`; // hex.toString => COLOR@[r,c] | COLOR@Skip , COLOR@Resign
173
+ }
174
+ /** hex.rcspString => COLOR@[ r, c] | 'COLOR@Skip ' , 'COLOR@Resign ' */
175
+ rcspString(sc = this.tile?.player?.color || this.meep?.player?.color) {
176
+ return `${sc ?? 'Empty'}@${this.rcsp}`;
177
+ }
178
+ }
179
+ /** One Hex cell in the game, shown as a polyStar Shape */
180
+ export class Hex2 extends Hex1 {
181
+ /** Child of mapCont.hexCont: HexCont holds hexShape(color), rcText, distText, capMark */
182
+ cont = new HexCont(this); // Hex IS-A Hex0, HAS-A HexCont Container
183
+ radius = TP.hexRad; // determines width & height
184
+ hexShape = this.makeHexShape(); // shown on this.cont: colored hexagon
185
+ get mapCont() { return this.map.mapCont; }
186
+ get markCont() { return this.mapCont.markCont; }
187
+ get x() { return this.cont.x; }
188
+ set x(v) { this.cont.x = v; }
189
+ get y() { return this.cont.y; }
190
+ set y(v) { this.cont.y = v; }
191
+ get scaleX() { return this.cont.scaleX; }
192
+ get scaleY() { return this.cont.scaleY; }
193
+ // if override set, then must override get!
194
+ get district() { return this._district; }
195
+ set district(d) {
196
+ this._district = d; // cannot use super.district = d [causes recursion, IIRC]
197
+ this.distText.text = `${d}`;
198
+ }
199
+ distColor; // district color of hexShape (paintHexShape)
200
+ distText; // shown on this.cont
201
+ rcText; // shown on this.cont
202
+ setUnit(unit, meep = false) {
203
+ const cont = this.map.mapCont.tileCont, x = this.x, y = this.y;
204
+ let k = true; // debug double tile
205
+ const this_unit = (meep ? this.meep : this.tile);
206
+ if (unit !== undefined && this_unit !== undefined && !(meep && this_unit.recycleVerb === 'demolished')) {
207
+ if (this === this_unit.source?.hex && this === unit.source?.hex) {
208
+ // Table.dragStart does moveTo(undefined); which triggers source.nextUnit()
209
+ // so if we drop to the startHex, we have a collision.
210
+ // Resolve by putting this_unit (the 'nextUnit') back in the source.
211
+ // (availUnit will recurse to set this.unit = undefined)
212
+ this_unit.source.availUnit(this_unit); // Meeple extends Tile, but TS seems confused.
213
+ }
214
+ else if (k)
215
+ debugger;
216
+ }
217
+ meep ? (super.meep = unit) : (super.tile = unit); // set _meep or _tile;
218
+ if (unit !== undefined) {
219
+ unit.x = x;
220
+ unit.y = y;
221
+ cont.addChild(unit); // meep will go under tile
222
+ // after source.hex is set, updateCounter:
223
+ if (this === unit.source?.hex)
224
+ unit.source.updateCounter();
225
+ }
226
+ }
227
+ get tile() { return super.tile; }
228
+ set tile(tile) { this.setUnit(tile, false); }
229
+ get meep() { return super.meep; }
230
+ set meep(meep) { this.setUnit(meep, true); }
231
+ /**
232
+ * add Hex2 to map?.mapCont.hexCont; not in map.hexAry!
233
+ * Hex2.cont contains:
234
+ * - polyStar Shape of radius @ (XY=0,0)
235
+ * - stoneIdText (user settable stoneIdText.text)
236
+ * - rcText (r,c)
237
+ * - distText (d)
238
+ */
239
+ constructor(map, row, col, name) {
240
+ super(map, row, col, name);
241
+ this.initCont(row, col);
242
+ map?.mapCont.hexCont.addChild(this.cont);
243
+ this.hexShape.name = this.Aname;
244
+ const nf = (n) => `${n !== undefined ? (n === Math.floor(n)) ? n : n.toFixed(1) : ''}`;
245
+ const rc = `${nf(row)},${nf(col)}`, tdy = -25;
246
+ const rct = this.rcText = new Text(rc, F.fontSpec(26), 'white'); // radius/2 ?
247
+ rct.textAlign = 'center';
248
+ rct.y = tdy; // based on fontSize? & radius
249
+ this.cont.addChild(rct);
250
+ this.distText = new Text(``, F.fontSpec(20));
251
+ this.distText.textAlign = 'center';
252
+ this.distText.y = tdy + 46; // yc + 26+20
253
+ this.cont.addChild(this.distText);
254
+ this.legalMark.setOnHex(this);
255
+ this.showText(true); // & this.cache()
256
+ }
257
+ /** set visibility of rcText & distText */
258
+ showText(vis = this.rcText.visible) {
259
+ this.rcText.visible = this.distText.visible = vis;
260
+ this.cont.updateCache();
261
+ }
262
+ legalMark = new LegalMark();
263
+ get isLegal() { return this._isLegal; }
264
+ set isLegal(v) {
265
+ super.isLegal = v;
266
+ this.legalMark.visible = v;
267
+ }
268
+ initCont(row, col) {
269
+ const cont = this.cont;
270
+ const { x, y, w, h } = this.xywh(this.radius, TP.useEwTopo, row, col); // include margin space between hexes
271
+ cont.x = x;
272
+ cont.y = y;
273
+ // initialize cache bounds:
274
+ cont.setBounds(-w / 2, -h / 2, w, h);
275
+ const b = cont.getBounds();
276
+ cont.cache(b.x, b.y, b.width, b.height);
277
+ // cont.rotation = this.map.topoRot;
278
+ }
279
+ makeHexShape(shape = HexShape) {
280
+ const hs = new shape(this.radius, this.map.topoRot);
281
+ this.cont.addChildAt(hs, 0);
282
+ this.cont.hitArea = hs;
283
+ hs.paint('grey');
284
+ return hs;
285
+ }
286
+ /** set hexShape using color: draw border and fill
287
+ * @param color
288
+ * @param district if supplied, set this.district
289
+ */
290
+ setHexColor(color, district) {
291
+ if (district !== undefined)
292
+ this.district = district; // hex.setHexColor update district
293
+ this.distColor = color;
294
+ this.hexShape.paint(color);
295
+ this.cont.updateCache();
296
+ }
297
+ // The following were created for the map in hexmarket:
298
+ /** unit distance between Hexes: adjacent = 1; see also: radialDist */
299
+ metricDist(hex) {
300
+ let { x: tx, y: ty } = this.xywh(1), { x: hx, y: hy } = hex.xywh(1);
301
+ let dx = tx - hx, dy = ty - hy;
302
+ return Math.sqrt(dx * dx + dy * dy); // tw == H.sqrt3
303
+ }
304
+ /** location of corner between dir0 and dir1; in parent coordinates.
305
+ * @param dir0 an EwDir
306
+ * @param dir1 an EwDir
307
+ */
308
+ // hexmarket uses to find ewDir corner between two nsDir edges.
309
+ cornerPoint(dir0, dir1) {
310
+ const d0 = H.ewDirRot[dir0], d1 = H.ewDirRot[dir1];
311
+ let a2 = (d0 + d1) / 2, h = this.radius;
312
+ if (Math.abs(d0 - d1) > 180)
313
+ a2 += 180;
314
+ let a = a2 * H.degToRadians;
315
+ return new Point(this.x + Math.sin(a) * h, this.y - Math.cos(a) * h);
316
+ }
317
+ /** location of edge point in dir; in parent coordinates. */
318
+ edgePoint(dir) {
319
+ let a = H.ewDirRot[dir] * H.degToRadians, h = this.radius * H.sqrt3_2;
320
+ return new Point(this.x + Math.sin(a) * h, this.y - Math.cos(a) * h);
321
+ }
322
+ }
323
+ export class RecycleHex extends Hex2 {
324
+ }
325
+ /** for contrast paint it black AND white, leave a hole in the middle unpainted. */
326
+ export class HexMark extends HexShape {
327
+ hexMap;
328
+ hex;
329
+ constructor(hexMap, radius, radius0 = 0) {
330
+ super(radius);
331
+ this.hexMap = hexMap;
332
+ const mark = this;
333
+ const cm = "rgba(127,127,127,.3)";
334
+ mark.graphics.f(cm).dp(0, 0, this.radius, 6, 0, this.tilt);
335
+ mark.cache(-radius, -radius, 2 * radius, 2 * radius);
336
+ mark.graphics.c().f(C.BLACK).dc(0, 0, radius0);
337
+ mark.updateCache("destination-out");
338
+ mark.setHexBounds();
339
+ mark.mouseEnabled = false;
340
+ }
341
+ paint(color) {
342
+ this.setHexBounds();
343
+ return this.graphics; // do not repaint.
344
+ }
345
+ // Fail: markCont to be 'above' tileCont...
346
+ showOn(hex) {
347
+ // when mark is NOT showing, this.visible === false && this.hex === undefined.
348
+ // when mark IS showing, this.visible === true && (this.hex instanceof Hex2)
349
+ if (this.hex === hex)
350
+ return;
351
+ if (this.hex) {
352
+ this.visible = false;
353
+ if (!this.hex.cont.cacheID)
354
+ debugger;
355
+ this.hex.cont.updateCache();
356
+ }
357
+ this.hex = hex;
358
+ if (this.hex) {
359
+ this.visible = true;
360
+ hex.cont.addChild(this);
361
+ if (!hex.cont.cacheID)
362
+ debugger;
363
+ hex.cont.updateCache();
364
+ }
365
+ this.hexMap.update();
366
+ }
367
+ }
368
+ export class MapCont extends Container {
369
+ hexMap;
370
+ constructor(hexMap) {
371
+ super();
372
+ this.hexMap = hexMap;
373
+ this.name = 'mapCont';
374
+ }
375
+ static cNames = ['resaCont', 'hexCont', 'infCont', 'tileCont', 'markCont', 'capCont', 'counterCont', 'eventCont'];
376
+ resaCont; // playerPanels
377
+ hexCont; // hex shapes on bottom stats: addChild(dsText), parent.rotation
378
+ infCont; // infMark below tileCont; Hex2.showInf
379
+ tileCont; // Tiles & Meeples on Hex2/HexMap.
380
+ markCont; // showMark over Hex2; LegalMark
381
+ capCont; // for tile.capMark
382
+ counterCont; // counters for AuctionCont
383
+ eventCont; // the eventHex & and whatever Tile is on it...
384
+ /** add all the layers of Containers. */
385
+ addContainers() {
386
+ MapCont.cNames.forEach(cname => {
387
+ const cont = new Container();
388
+ cont.Aname = cont.name = cname;
389
+ this[cname] = cont;
390
+ this.addChild(cont);
391
+ });
392
+ }
393
+ }
394
+ /**
395
+ * Collection of Hex *and* Graphics-Containers for Hex2
396
+ * allStones: HSC[] and districts: Hex[]
397
+ *
398
+ * HexMap[row][col]: Hex or Hex2 elements.
399
+ * If mapCont is set, then populate with Hex2
400
+ *
401
+ * (TP.mh X TP.nh) hexes in districts; allStones: HSC[]
402
+ *
403
+ * With a Mark and off-map: skipHex & resignHex
404
+ *
405
+ */
406
+ export class HexMap extends Array {
407
+ hexC;
408
+ // A color for each District: 'rgb(198,198,198)'
409
+ static distColor = ['lightgrey', "limegreen", "deepskyblue", "rgb(255,165,0)", "violet", "rgb(250,80,80)", "yellow"];
410
+ get asHex2Map() { return this; }
411
+ /** Each occupied Hex, with the occupying PlayerColor */
412
+ district = [];
413
+ hexAry; // set by makeAllDistricts()
414
+ mapCont = new MapCont(this.asHex2Map); // if/when using Hex2
415
+ //
416
+ // | // | // |
417
+ // 2 . | 1 // 1 . | .5 // 2/sqrt3 . | 1/sqrt3
418
+ // . | // . | // . |
419
+ // . | // . | // . |
420
+ // -----------------------+ // -----------------------+ // -----------------------+
421
+ // sqrt3 // sqrt3/2 // 1
422
+ //
423
+ radius = TP.hexRad;
424
+ /** return this.centerHex.xywh() for this.topo */
425
+ get xywh() { return this.centerHex.xywh(); }
426
+ minCol = undefined; // Array.forEach does not look at negative indices!
427
+ maxCol = undefined; // used by rcLinear
428
+ minRow = undefined; // to find centerHex
429
+ maxRow = undefined; // to find centerHex
430
+ get centerHex() {
431
+ let cr = Math.floor(((this.maxRow ?? 0) + (this.minRow ?? 0)) / 2);
432
+ let cc = Math.floor(((this.minCol ?? 0) + (this.maxCol ?? 0)) / 2);
433
+ return this[cr][cc]; // as Hex2; as T;
434
+ }
435
+ // when called, maxRow, etc are defined...
436
+ get nRowCol() { return [(this.maxRow ?? 0) - (this.minRow ?? 0), (this.maxCol ?? 0) - (this.minCol ?? 0)]; }
437
+ getCornerHex(dn) {
438
+ return this.centerHex.lastHex(dn);
439
+ }
440
+ rcLinear(row, col) { return col + row * (1 + (this.maxCol ?? 0) - (this.minCol ?? 0)); }
441
+ metaMap = Array(); // hex0 (center Hex) of each MetaHex, has metaLinks to others.
442
+ mark; // a cached DisplayObject, used by showMark
443
+ Aname = '';
444
+ /**
445
+ * HexMap: TP.nRows X TP.nCols hexes.
446
+ *
447
+ * Basic map is non-GUI, addToMapCont uses Hex2 elements to enable GUI interaction.
448
+ * @param addToMapCont use Hex2 for Hex, make Containers: hexCont, infCont, markCont, stoneCont
449
+ * @param hexC Constructor<T> for the Hex elements (typed as HexConstructor<Hex> for Typescript...)
450
+ */
451
+ constructor(radius = TP.hexRad, addToMapCont = false, hexC = Hex) {
452
+ super(); // Array<Array<Hex>>()
453
+ this.hexC = hexC;
454
+ this.radius = radius;
455
+ if (addToMapCont)
456
+ this.addToMapCont(this.hexC);
457
+ }
458
+ // the 'tilt' to apply to a HexShape to align with map.topo:
459
+ get topoRot() { return TP.useEwTopo ? 30 : 0; }
460
+ makeMark() {
461
+ const mark = new HexMark(this.asHex2Map, this.radius, this.radius / 2.5);
462
+ return mark;
463
+ }
464
+ /** create/attach Graphical components for HexMap */
465
+ addToMapCont(hexC) {
466
+ if (hexC)
467
+ this.hexC = hexC;
468
+ this.mark = this.makeMark();
469
+ this.mapCont.addContainers();
470
+ return this;
471
+ }
472
+ /** ...stage.update() */
473
+ update() {
474
+ this.mapCont.hexCont.updateCache(); // when toggleText: hexInspector
475
+ this.mapCont.hexCont.parent?.stage.update();
476
+ }
477
+ /** to build this HexMap: create Hex (or Hex2) and link it to neighbors. */
478
+ addHex(row, col, district, hexC = this.hexC) {
479
+ // If we have an on-screen Container, then use Hex2: (addToMapCont *before* makeAllDistricts)
480
+ const hex = new hexC(this, row, col);
481
+ hex.district = district; // and set Hex2.districtText
482
+ if (this[row] === undefined) { // create new row array
483
+ this[row] = new Array();
484
+ if (this.minRow === undefined || row < this.minRow)
485
+ this.minRow = row;
486
+ if (this.maxRow === undefined || row > this.maxRow)
487
+ this.maxRow = row;
488
+ }
489
+ if (this.minCol === undefined || col < this.minCol)
490
+ this.minCol = col;
491
+ if (this.maxCol === undefined || col > this.maxCol)
492
+ this.maxCol = col;
493
+ this[row][col] = hex; // addHex to this Array<Array<Hex>>
494
+ this.link(hex); // link to existing neighbors
495
+ return hex;
496
+ }
497
+ hexUnderObj(dragObj, legalOnly = true) {
498
+ const pt = dragObj.parent.localToLocal(dragObj.x, dragObj.y, this.mapCont.markCont);
499
+ return this.hexUnderPoint(pt.x, pt.y, legalOnly);
500
+ }
501
+ /** find first Hex matching the given predicate function */
502
+ findHex(fn) {
503
+ for (let hexRow of this) {
504
+ if (hexRow === undefined)
505
+ continue;
506
+ const found = hexRow.find((hex) => hex && fn(hex));
507
+ if (found !== undefined)
508
+ return found;
509
+ }
510
+ return undefined;
511
+ }
512
+ /** Array.forEach does not use negative indices: ASSERT [row,col] is non-negative (so 'of' works) */
513
+ forEachHex(fn) {
514
+ // minRow generally [0 or 1] always <= 5, so not worth it
515
+ //for (let ir = this.minRow || 0; ir < this.length; ir++) {
516
+ for (let ir of this) {
517
+ // beginning and end of this AND ir may be undefined
518
+ if (ir !== undefined)
519
+ for (let hex of ir) {
520
+ hex !== undefined && fn(hex);
521
+ }
522
+ }
523
+ }
524
+ /** return array of results of mapping fn over each Hex */
525
+ mapEachHex(fn) {
526
+ const rv = [];
527
+ this.forEachHex(hex => rv.push(fn(hex)));
528
+ return rv;
529
+ }
530
+ /** find all Hexes matching given predicate */
531
+ filterEachHex(fn) {
532
+ const rv = [];
533
+ this.forEachHex(hex => fn(hex) && rv.push(hex));
534
+ return rv;
535
+ }
536
+ /** make this.mark visible above the given Hex */
537
+ showMark(hex) {
538
+ const mark = this.mark;
539
+ if (!hex) { // || hex.Aname === S_Skip || hex.Aname === S_Resign) {
540
+ mark.visible = false;
541
+ }
542
+ else if (hex instanceof Hex2) {
543
+ mark.scaleX = hex.scaleX;
544
+ mark.scaleY = hex.scaleY;
545
+ mark.visible = true;
546
+ // put the mark, at location of hex, on hex.markCont:
547
+ hex.cont.localToLocal(0, 0, hex.markCont, mark);
548
+ hex.markCont.addChild(mark);
549
+ this.update();
550
+ }
551
+ }
552
+ /** neighborhood topology, E-W & N-S orientation; even(n0) & odd(n1) rows: */
553
+ topo = TP.useEwTopo ? H.ewTopo : H.nsTopo;
554
+ /** see also: Hex.linkDirs */
555
+ get linkDirs() {
556
+ return TP.useEwTopo ? H.ewDirs : H.nsDirs;
557
+ }
558
+ nextRowCol(rc, dir, nt = this.topo(rc)) {
559
+ const ntdir = nt[dir];
560
+ const { dr, dc } = ntdir; // OR (nt as TopoEW[dir as EwDir]) OR simply: nt[dir]
561
+ let row = rc.row + dr, col = rc.col + dc;
562
+ return { row, col };
563
+ }
564
+ /** link hex to/from each extant neighor */
565
+ link(hex, rc = hex, map = this, nt = this.topo(rc), lf = (hex) => hex.links) {
566
+ const topoDirs = Object.keys(nt);
567
+ topoDirs.forEach(dir => {
568
+ const { dr, dc } = nt[dir]; // OR (nt as TopoEW[dir as EwDir])
569
+ const nr = rc.row + dr;
570
+ const nc = rc.col + dc;
571
+ const nHex = map[nr] && map[nr][nc];
572
+ if (!!nHex) {
573
+ lf(hex)[dir] = nHex;
574
+ lf(nHex)[H.dirRev[dir]] = hex;
575
+ }
576
+ });
577
+ }
578
+ /**
579
+ * The [Legal] Hex under the given x,y coordinates.
580
+ * If on the line, then the top (last drawn) Hex.
581
+ * @param x in local coordinates of this HexMap.mapCont
582
+ * @param y
583
+ * @param legal - returnn ONLY hex with LegalMark visible & mouseenabled.
584
+ * @returns the Hex2 under mouse or undefined, if not a Hex (background)
585
+ */
586
+ hexUnderPoint(x, y, legal = true) {
587
+ const mark = this.mapCont.markCont.getObjectUnderPoint(x, y, 1);
588
+ // Note: in theory, mark could be on a Hex2 that is NOT in hexCont!
589
+ if (mark instanceof LegalMark)
590
+ return mark.hex2;
591
+ if (legal)
592
+ return undefined;
593
+ const hexc = this.mapCont.hexCont.getObjectUnderPoint(x, y, 1); // 0=all, 1=mouse-enabled (Hex, not Stone)
594
+ if (hexc instanceof HexCont)
595
+ return hexc.hex2;
596
+ return undefined;
597
+ }
598
+ // not sure if these will be useful:
599
+ _nh;
600
+ _mh;
601
+ get nh() { return this._nh; }
602
+ get mh() { return this._mh; }
603
+ /**
604
+ *
605
+ * @param nh number of hexes on on edge of metaHex
606
+ * @param mh order of metaHexes (greater than 0);
607
+ */
608
+ makeAllDistricts(nh = TP.nHexes, mh = TP.mHexes) {
609
+ this._nh = nh;
610
+ this._mh = mh;
611
+ const hexAry = this.makeDistrict(nh, 0, mh, 0); // nh hexes on outer ring; single meta-hex
612
+ this.mapCont.hexCont && this.centerOnContainer();
613
+ this.hexAry = hexAry;
614
+ return hexAry;
615
+ }
616
+ centerOnContainer() {
617
+ let mapCont = this.mapCont;
618
+ let hexRect = mapCont.hexCont.getBounds(); // based on aggregate of Hex2.cont.cache(bounds);
619
+ const { x, y, width, height } = hexRect;
620
+ let x0 = x + width / 2, y0 = y + height / 2;
621
+ MapCont.cNames.forEach(cname => {
622
+ const cont = mapCont[cname];
623
+ cont.x = -x0;
624
+ cont.y = -y0;
625
+ });
626
+ // mapCont.x = x0; mapCont.y = y0;
627
+ }
628
+ pickColor(hexAry) {
629
+ let hex = hexAry[0];
630
+ let adjColor = [HexMap.distColor[0]]; // colors not to use
631
+ this.linkDirs.forEach(hd => {
632
+ let nhex = hex;
633
+ while (!!(nhex = nhex.nextHex(hd))) {
634
+ if (nhex.district != hex.district) {
635
+ adjColor.push(nhex.distColor);
636
+ return;
637
+ }
638
+ }
639
+ });
640
+ return HexMap.distColor.find(ci => !adjColor.includes(ci)) ?? 'white'; // or undefined or ...
641
+ }
642
+ /**
643
+ * rings of Hex with EwTopo; HexShape(tilt = 'NE')
644
+ * @param nh order of inner-hex: number hexes on side of meta-hex
645
+ * @param district identifying number of this district
646
+ * @param mr make new district on meta-row
647
+ * @param mc make new district on meta-col
648
+ */
649
+ makeDistrict(nh, district, mr, mc) {
650
+ const mcp = Math.abs(mc % 2), mrp = Math.abs(mr % 2), dia = 2 * nh - 1;
651
+ // irow-icol define topology of MetaHex composed of HexDistrict
652
+ // TODO: generalize using this.topo to compute offsets!
653
+ const irow = (mr, mc) => {
654
+ let ir = mr * dia - nh * (mcp + 1) + 1;
655
+ ir -= Math.floor((mc) / 2); // - half a row for each metaCol
656
+ return ir;
657
+ };
658
+ const icol = (mr, mc, row) => {
659
+ let np = Math.abs(nh % 2), rp = Math.abs(row % 2);
660
+ let ic = Math.floor(mc * ((nh * 3 - 1) / 2));
661
+ ic += (nh - 1); // from left edge to center
662
+ ic -= Math.floor((mc + (2 - np)) / 4); // 4-metaCol means 2-rows, mean 1-col
663
+ ic += Math.floor((mr - rp) / 2); // 2-metaRow means +1 col
664
+ return ic;
665
+ };
666
+ const row0 = irow(mr, mc), col0 = icol(mr, mc, row0);
667
+ const hexAry = Array();
668
+ hexAry['Mr'] = mr;
669
+ hexAry['Mc'] = mc;
670
+ const hex = this.addHex(row0, col0, district);
671
+ hexAry.push(hex); // The *center* hex
672
+ let rc = { row: row0, col: col0 }; // == {hex.row, hex.col}
673
+ //console.groupCollapsed(`makelDistrict [mr: ${mr}, mc: ${mc}] hex0= ${hex.Aname}:${district}-${dcolor}`)
674
+ //console.log(`.makeDistrict: [mr: ${mr}, mc: ${mc}] hex0= ${hex.Aname}`, hex)
675
+ const dirs = this.linkDirs; // HexDirs of the extant Topo.
676
+ const startDir = dirs.includes('W') ? 'W' : 'WN'; // 'W' or 'WN'
677
+ for (let ring = 1; ring < nh; ring++) {
678
+ rc = this.nextRowCol(rc, startDir); // step West to start a ring
679
+ // place 'ring' hexes along each axis-line:
680
+ dirs.forEach(dir => rc = this.newHexesOnLine(ring, rc, dir, district, hexAry));
681
+ }
682
+ //console.groupEnd()
683
+ this.setDistrictColor(hexAry, district);
684
+ return hexAry;
685
+ }
686
+ setDistrictColor(hexAry, district = 0) {
687
+ this.district[district] = hexAry;
688
+ if (hexAry[0] instanceof Hex2) {
689
+ const hex2Ary = hexAry;
690
+ const dcolor = district == 0 ? HexMap.distColor[0] : this.pickColor(hex2Ary);
691
+ hex2Ary.forEach(hex => hex.setHexColor(dcolor)); // makeDistrict: dcolor=lightgrey
692
+ }
693
+ }
694
+ /**
695
+ *
696
+ * @param n number of Hex to create
697
+ * @param hex start with a Hex to the West of this Hex
698
+ * @param dir after first Hex move this Dir for each other hex
699
+ * @param district
700
+ * @param hexAry push created Hex(s) on this array
701
+ * @returns RC of next Hex to create (==? RC of original hex)
702
+ */
703
+ newHexesOnLine(n, rc, dir, district, hexAry) {
704
+ let hex;
705
+ for (let i = 0; i < n; i++) {
706
+ hexAry.push(hex = this.addHex(rc.row, rc.col, district));
707
+ rc = this.nextRowCol(hex, dir);
708
+ }
709
+ return rc;
710
+ }
711
+ }
712
+ /** Marker class for HexMap used by GamePlayD */
713
+ export class HexMapD extends HexMap {
714
+ }