@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/table.js ADDED
@@ -0,0 +1,707 @@
1
+ import { AT, C, CenterText, Dragger, F, KeyBinder, S, ScaleableContainer, stime } from "@thegraid/easeljs-lib";
2
+ import { Container, EventDispatcher, Graphics, Shape, Text } from "@thegraid/easeljs-module";
3
+ import { NamedContainer } from "./game-play";
4
+ import { Hex2 } from "./hex";
5
+ import { Player } from "./player";
6
+ import { PlayerPanel } from "./player-panel";
7
+ import { CircleShape, HexShape, RectShape, UtilButton } from "./shapes";
8
+ import { playerColor0, playerColor1, TP } from "./table-params";
9
+ import { Tile } from "./tile";
10
+ //import { TablePlanner } from "./planner";
11
+ function firstChar(s, uc = true) { return uc ? s.substring(0, 1).toUpperCase() : s.substring(0, 1); }
12
+ ;
13
+ /** to own file... */
14
+ class TablePlanner {
15
+ constructor(gamePlay) { }
16
+ }
17
+ class TextLog extends NamedContainer {
18
+ size;
19
+ lead;
20
+ constructor(Aname, nlines = 6, size = 30, lead = 3) {
21
+ super(Aname);
22
+ this.size = size;
23
+ this.lead = lead;
24
+ this.lines = new Array(nlines);
25
+ for (let ndx = 0; ndx < nlines; ndx++)
26
+ this.lines[ndx] = this.newText(`//0:`);
27
+ this.addChild(...this.lines);
28
+ }
29
+ lines;
30
+ lastLine = '';
31
+ nReps = 0;
32
+ height(n = this.lines.length) {
33
+ return (this.size + this.lead) * n;
34
+ }
35
+ clear() {
36
+ this.lines.forEach(tline => tline.text = '');
37
+ this.stage?.update();
38
+ }
39
+ newText(line = '') {
40
+ const text = new Text(line, F.fontSpec(this.size));
41
+ text.textAlign = 'left';
42
+ text.mouseEnabled = false;
43
+ return text;
44
+ }
45
+ spaceLines(cy = 0, lead = this.lead) {
46
+ this.lines.forEach(tline => (tline.y = cy, cy += tline.getMeasuredLineHeight() + lead));
47
+ }
48
+ log(line, from = '', toConsole = true) {
49
+ line = line.replace('/\n/g', '-');
50
+ toConsole && console.log(stime(`${from}:`), line);
51
+ if (line === this.lastLine) {
52
+ this.lines[this.lines.length - 1].text = `[${++this.nReps}] ${line}`;
53
+ }
54
+ else {
55
+ this.removeChild(this.lines.shift()); // assert is not undefined
56
+ this.lines.push(this.addChild(this.newText(line)));
57
+ this.spaceLines();
58
+ this.lastLine = line;
59
+ this.nReps = 0;
60
+ }
61
+ this.stage?.update();
62
+ return line;
63
+ }
64
+ }
65
+ /** layout display components, setup callbacks to GamePlay */
66
+ export class Table extends EventDispatcher {
67
+ static table;
68
+ static stageTable(obj) {
69
+ return obj.stage.table;
70
+ }
71
+ namedOn(Aname, type, listener, scope, once, data, useCapture = false) {
72
+ const list2 = this.on(type, listener, scope, once, data, useCapture);
73
+ list2.Aname = Aname;
74
+ }
75
+ gamePlay;
76
+ stage;
77
+ bgRect;
78
+ hexMap; // from gamePlay.hexMap
79
+ undoCont = new NamedContainer('undoCont');
80
+ undoShape = new Shape();
81
+ skipShape = new Shape();
82
+ redoShape = new Shape();
83
+ undoText = new Text('', F.fontSpec(30)); // length of undo stack
84
+ redoText = new Text('', F.fontSpec(30)); // length of history stack
85
+ winText = new Text('', F.fontSpec(40), 'green');
86
+ winBack = new Shape(new Graphics().f(C.nameToRgbaString("lightgrey", .6)).r(-180, -5, 360, 130));
87
+ dragger;
88
+ overlayCont = new Container();
89
+ constructor(stage) {
90
+ super();
91
+ this.overlayCont.name = 'overlay';
92
+ // backpointer so Containers can find their Table (& curMark)
93
+ Table.table = stage.table = this;
94
+ this.stage = stage;
95
+ this.scaleCont = this.makeScaleCont(!!this.stage.canvas); // scaleCont & background
96
+ this.scaleCont.addChild(this.overlayCont); // will add again at top/end of the list.
97
+ }
98
+ /** shows the last 2 start of turn lines */
99
+ turnLog = new TextLog('turnLog', 2);
100
+ /** show [13] other interesting log strings */
101
+ textLog = new TextLog('textLog', TP.textLogLines);
102
+ logTurn(line) {
103
+ this.turnLog.log(line, 'table.logTurn'); // in top two lines
104
+ }
105
+ logText(line, from = '') {
106
+ const text = this.textLog.log(`${this.gamePlay.turnNumber}: ${line}`, from || '***'); // scrolling lines below
107
+ this.gamePlay.logWriter.writeLine(`// ${text}`);
108
+ // JSON string, instead of JSON5 comment:
109
+ // const text = this.textLog.log(`${this.gamePlay.turnNumber}: ${line}`, from); // scrolling lines below
110
+ // this.gamePlay.logWriter.writeLine(`"${line}",`);
111
+ }
112
+ setupUndoButtons(xOffs, bSize, skipRad, bgr, row = 8, col = -7) {
113
+ const undoC = this.undoCont; // holds the undo buttons.
114
+ this.setToRowCol(undoC, row, col);
115
+ const progressBg = new Shape(), bgw = 200, bgym = 140, y0 = 0; // bgym = 240
116
+ const bgc = C.nameToRgbaString(TP.bgColor, .8);
117
+ progressBg.graphics.f(bgc).r(-bgw / 2, y0, bgw, bgym - y0);
118
+ undoC.addChildAt(progressBg, 0);
119
+ this.enableHexInspector(30);
120
+ this.dragger.makeDragable(undoC);
121
+ if (true && xOffs > 0)
122
+ return;
123
+ this.skipShape.graphics.f("white").dp(0, 0, 40, 4, 0, skipRad);
124
+ this.undoShape.graphics.f("red").dp(-xOffs, 0, bSize, 3, 0, 180);
125
+ this.redoShape.graphics.f("green").dp(+xOffs, 0, bSize, 3, 0, 0);
126
+ this.undoText.x = -52;
127
+ this.undoText.textAlign = "center";
128
+ this.redoText.x = 52;
129
+ this.redoText.textAlign = "center";
130
+ this.winText.x = 0;
131
+ this.winText.textAlign = "center";
132
+ undoC.addChild(this.skipShape);
133
+ undoC.addChild(this.undoShape);
134
+ undoC.addChild(this.redoShape);
135
+ undoC.addChild(this.undoText);
136
+ this.undoText.y = -14;
137
+ undoC.addChild(this.redoText);
138
+ this.redoText.y = -14;
139
+ let bgrpt = this.bgRect.parent.localToLocal(bgr.x, bgr.h, undoC);
140
+ this.undoText.mouseEnabled = this.redoText.mouseEnabled = false;
141
+ let aiControl = this.aiControl('pink', 75);
142
+ aiControl.x = 0;
143
+ aiControl.y = 100;
144
+ let pmy = 0;
145
+ undoC.addChild(aiControl);
146
+ undoC.addChild(this.winBack);
147
+ undoC.addChild(this.winText);
148
+ this.winText.y = Math.min(pmy, bgrpt.y); // 135 = winBack.y = winBack.h
149
+ this.winBack.visible = this.winText.visible = false;
150
+ this.winBack.x = this.winText.x;
151
+ this.winBack.y = this.winText.y;
152
+ }
153
+ showWinText(msg, color = 'green') {
154
+ this.winText.text = msg || "COLOR WINS:\nSTALEMATE (10 -- 10)\n0 -- 0";
155
+ this.winText.color = color;
156
+ this.winText.visible = this.winBack.visible = true;
157
+ this.hexMap.update();
158
+ }
159
+ enableHexInspector(qY = 52, cont = this.undoCont) {
160
+ const qShape = new HexShape(TP.hexRad / 3);
161
+ qShape.paint(C.BLACK);
162
+ qShape.y = qY; // size of 'skip' Triangles
163
+ cont.addChild(qShape);
164
+ this.dragger.makeDragable(qShape, this,
165
+ // dragFunc:
166
+ (qShape, ctx) => { },
167
+ // dropFunc:
168
+ (qShape, ctx) => {
169
+ this.downClick = true;
170
+ const hex = this.hexUnderObj(qShape, false); // also check hexCont!
171
+ qShape.x = 0;
172
+ qShape.y = qY; // return to regular location
173
+ cont.addChild(qShape);
174
+ if (!hex)
175
+ return;
176
+ const info = hex; //{ hex, stone: hex.playerColor, InfName }
177
+ console.log(`HexInspector:`, hex.Aname, info);
178
+ });
179
+ qShape.on(S.click, () => this.toggleText(), this); // toggle visible
180
+ }
181
+ downClick = false;
182
+ isVisible = false;
183
+ /** method invokes closure defined in enableHexInspector. */
184
+ toggleText(vis) {
185
+ if (this.downClick)
186
+ return (this.downClick = false, undefined); // skip one 'click' when pressup/dropfunc
187
+ if (vis === undefined)
188
+ vis = this.isVisible = !this.isVisible;
189
+ Tile.allTiles.forEach(tile => tile.textVis(vis));
190
+ this.hexMap.forEachHex(hex => hex.showText(vis));
191
+ this.hexMap.update(); // after toggleText & updateCache()
192
+ return undefined;
193
+ }
194
+ aiControl(color = TP.bgColor, dx = 100, rad = 16) {
195
+ let table = this;
196
+ // c m v on buttons
197
+ let makeButton = (dx, bc = TP.bgColor, tc = TP.bgColor, text, key = text) => {
198
+ let cont = new Container();
199
+ cont.name = 'aiControl';
200
+ let circ = new Graphics().f(bc).drawCircle(0, 0, rad);
201
+ let txt = new Text(text, F.fontSpec(rad), tc);
202
+ txt.y = -rad / 2;
203
+ txt.textAlign = 'center';
204
+ txt.mouseEnabled = false;
205
+ cont.x = dx;
206
+ cont.addChild(new Shape(circ));
207
+ cont.addChild(txt);
208
+ cont.on(S.click, (ev) => { KeyBinder.keyBinder.dispatchChar(key); });
209
+ return cont;
210
+ };
211
+ let bpanel = new Container();
212
+ bpanel.name = 'bpanel';
213
+ let c0 = TP.colorScheme[playerColor0], c1 = TP.colorScheme[playerColor1];
214
+ let cm = "rgba(100,100,100,.5)";
215
+ let bc = makeButton(-dx, c0, c1, 'C', 'c');
216
+ let bv = makeButton(dx, c1, c0, 'V', 'v');
217
+ let bm = makeButton(0, cm, C.BLACK, 'M', 'm');
218
+ bm.y -= 10;
219
+ let bn = makeButton(0, cm, C.BLACK, 'N', 'n');
220
+ bn.y += rad * 2;
221
+ let bs = makeButton(0, cm, C.BLACK, ' ', ' ');
222
+ bs.y += rad * 5;
223
+ bpanel.addChild(bc);
224
+ bpanel.addChild(bv);
225
+ bpanel.addChild(bm);
226
+ bpanel.addChild(bn);
227
+ bpanel.addChild(bs);
228
+ return bpanel;
229
+ }
230
+ /** all the non-map hexes created by newHex2 */
231
+ newHexes = [];
232
+ newHex2(row = 0, col = 0, name, claz = Hex2, sy = 0) {
233
+ const hex = new claz(this.hexMap, row, col, name);
234
+ hex.distText.text = name;
235
+ if (row <= 0) {
236
+ hex.y += (sy + row * .5 - .75) * (this.hexMap.radius);
237
+ }
238
+ this.newHexes.push(hex);
239
+ return hex;
240
+ }
241
+ noRowHex(name, crxy, claz) {
242
+ const { row, col } = crxy;
243
+ const hex = this.newHex2(row, col, name, claz);
244
+ return hex;
245
+ }
246
+ /**
247
+ *
248
+ * @param x0 frame left (* colw); relative to scaleCont
249
+ * @param y0 frame top (* rowh); relative to scaleCont
250
+ * @param w0 pad width (* colw);
251
+ * @param h0 pad height (* rowh)
252
+ * @param dh
253
+ * @returns
254
+ */
255
+ bgXYWH(x0 = -1, y0 = .5, w0 = 10, h0 = 1, dw = 0, dh = 0) {
256
+ const hexMap = this.hexMap;
257
+ // hexCont is offset to be centered on mapCont (center of hexCont is at mapCont[0,0])
258
+ // mapCont is offset [0,0] to scaleCont
259
+ const mapCont = hexMap.mapCont, hexCont = mapCont.hexCont; // local reference
260
+ this.scaleCont.addChild(mapCont);
261
+ // background sized for hexMap:
262
+ const { width, height } = hexCont.getBounds();
263
+ const { dxdc, dydr } = hexMap.xywh;
264
+ const xywh = { x: x0 * dxdc, y: y0 * dydr, w: width + w0 * dxdc, h: height + h0 * dydr };
265
+ // align center of mapCont(0,0) == hexMap(center) with center of background
266
+ mapCont.x = xywh.x + (xywh.w) / 2;
267
+ mapCont.y = xywh.y + (xywh.h) / 2;
268
+ xywh.w += dw * dxdc;
269
+ xywh.h += dh * dydr;
270
+ return xywh;
271
+ }
272
+ layoutTable(gamePlay) {
273
+ this.gamePlay = gamePlay;
274
+ const hexMap = this.hexMap = gamePlay.hexMap; // as AnkhMap<AnkhHex>
275
+ hexMap.addToMapCont(); // addToMapCont; make AnkhHex
276
+ hexMap.makeAllDistricts(); //
277
+ this.gamePlay.recycleHex = this.makeRecycleHex(TP.nHexes + 3.2);
278
+ const xywh = this.bgXYWH(); // override bgXYHW() to supply default/arg values
279
+ const hexCont = this.hexMap.mapCont.hexCont, hexp = this.scaleCont;
280
+ this.bgRect = this.setBackground(this.scaleCont, xywh); // bounded by xywh
281
+ const { x, y, width, height } = hexCont.getBounds();
282
+ hexCont.cache(x, y, width, height); // cache hexCont (bounded by bgr)
283
+ this.layoutTable2(); // supply args (mapCont?) if necessary; make overlays, score panel, extra tracks (auction...)
284
+ this.makePerPlayer();
285
+ this.setupUndoButtons(55, 60, 45, xywh); // & enableHexInspector()
286
+ const initialVis = false;
287
+ this.stage.on('drawend', () => setTimeout(() => this.toggleText(initialVis), 10), this, true);
288
+ this.hexMap.update();
289
+ // position turnLog & textLog
290
+ {
291
+ const parent = this.scaleCont, colx = -12;
292
+ this.setToRowCol(this.turnLog, 4, colx);
293
+ this.setToRowCol(this.textLog, 4, colx);
294
+ this.textLog.y += this.turnLog.height(Player.allPlayers.length + 1); // allow room for 1 line per player
295
+ parent.addChild(this.turnLog, this.textLog);
296
+ parent.stage.update();
297
+ }
298
+ this.namedOn("playerMoveEvent", S.add, this.gamePlay.playerMoveEvent, this.gamePlay);
299
+ }
300
+ // col locations, left-right mirrored:
301
+ colf(pIndex, icol, row) {
302
+ const dc = 10 - Math.abs(row) % 2;
303
+ const col = (pIndex == 0 ? (icol) : (dc - icol));
304
+ return { row, col };
305
+ }
306
+ layoutTable2() {
307
+ }
308
+ get panelHeight() { return (2 * TP.nHexes - 1) / 3 - .2; }
309
+ // col==0 is on left edge of hexMap; The *center* hex is col == (nHexes-1)
310
+ panelLoc(pIndex, np = Math.min(Player.allPlayers.length, 6), r0 = this.hexMap.centerHex.row, dr = this.panelHeight + .2) {
311
+ const nh1 = this.hexMap.centerHex.col, coff = TP.nHexes + 2;
312
+ const c0 = nh1 - coff, c1 = nh1 + coff;
313
+ const locs = [
314
+ [r0 - dr, c0, +1], [r0, c0, +1], [r0 + dr, c0, +1],
315
+ [r0 - dr, c1, -1], [r0, c1, -1], [r0 + dr, c1, -1]
316
+ ];
317
+ const seq = [[], [0], [0, 3], [0, 3, 1], [0, 3, 4, 1], [0, 3, 4, 2, 1], [0, 3, 4, 5, 2, 1]];
318
+ const seqn = seq[np], ndx = seqn[Math.min(pIndex, np - 1)];
319
+ return locs[ndx];
320
+ }
321
+ allPlayerPanels = [];
322
+ /** make player panels, placed at locations... */
323
+ makePerPlayer() {
324
+ this.allPlayerPanels.length = 0; // TODO: maybe deconstruct
325
+ const high = this.panelHeight, wide = 4.5;
326
+ Player.allPlayers.forEach((player, pIndex) => {
327
+ const [row, col, dir] = this.panelLoc(pIndex);
328
+ this.allPlayerPanels[pIndex] = player.panel = new PlayerPanel(this, player, high, wide, row - high / 2, col - wide / 2, dir);
329
+ player.makePlayerBits();
330
+ });
331
+ }
332
+ /** move cont to nominal [row, col] of hexCont */
333
+ setToRowCol(cont, row = 0, col = 0) {
334
+ if (!cont.parent)
335
+ this.scaleCont.addChild(cont);
336
+ const hexCont = this.hexMap.mapCont.hexCont;
337
+ //if (cont.parent === hexCont) debugger;
338
+ const hexC = this.hexMap.centerHex;
339
+ const { x, y, dxdc, dydr } = hexC.xywh();
340
+ const xx = x + (col - hexC.col) * dxdc;
341
+ const yy = y + (row - hexC.row) * dydr;
342
+ hexCont.localToLocal(xx, yy, cont.parent, cont);
343
+ if (cont.parent === hexCont) {
344
+ cont.x = xx;
345
+ cont.y = yy;
346
+ }
347
+ }
348
+ sourceOnHex(source, hex) {
349
+ if (source?.counter)
350
+ source.counter.mouseEnabled = false;
351
+ hex.legalMark.setOnHex(hex);
352
+ hex.cont.visible = false;
353
+ }
354
+ makeCircleButton(color = C.WHITE, rad = 20, c, fs = 30) {
355
+ const button = new Container();
356
+ button.name = 'circle';
357
+ const shape = new CircleShape(color, rad, '');
358
+ button.addChild(shape);
359
+ if (c) {
360
+ const t = new CenterText(c, fs);
361
+ t.y += 2;
362
+ button.addChild(t);
363
+ }
364
+ button.setBounds(-rad, -rad, rad * 2, rad * 2);
365
+ button.mouseEnabled = false;
366
+ return button;
367
+ }
368
+ makeSquareButton(color = C.WHITE, xywh, c, fs = 30) {
369
+ const button = new Container();
370
+ button.name = 'square';
371
+ const shape = new RectShape(xywh, color, '');
372
+ button.addChild(shape);
373
+ if (c) {
374
+ const t = new CenterText(c, fs);
375
+ t.y += 2;
376
+ button.addChild(t);
377
+ }
378
+ shape.mouseEnabled = false;
379
+ return button;
380
+ }
381
+ makeRecycleHex(row = TP.nHexes + 3.2, col = 0) {
382
+ const name = 'Recycle';
383
+ const image = new Tile(name).addImageBitmap(name); // ignore Tile, get image.
384
+ image.y = -TP.hexRad / 2; // recenter
385
+ const rHex = this.newHex2(row, col, name, Hex2);
386
+ this.setToRowCol(rHex.cont, row, col);
387
+ rHex.rcText.visible = rHex.distText.visible = false;
388
+ rHex.setHexColor(C.WHITE);
389
+ rHex.cont.addChild(image);
390
+ rHex.cont.updateCache();
391
+ return rHex;
392
+ }
393
+ doneButton;
394
+ doneClicked = (evt) => {
395
+ if (this.doneButton)
396
+ this.doneButton.visible = false;
397
+ this.gamePlay.phaseDone(); // <--- main doneButton does not supply 'panel'
398
+ };
399
+ addDoneButton(actionCont, rh) {
400
+ const w = 90, h = 56;
401
+ const doneButton = this.doneButton = new UtilButton('lightgreen', 'Done', 36, C.black);
402
+ doneButton.name = 'doneButton';
403
+ doneButton.x = -(w);
404
+ doneButton.y = 3 * rh;
405
+ doneButton.label.textAlign = 'right';
406
+ doneButton.on(S.click, this.doneClicked, this);
407
+ actionCont.addChild(doneButton);
408
+ // prefix advice: set text color
409
+ const o_cgf = doneButton.shape.cgf;
410
+ const cgf = (color) => {
411
+ const tcolor = (C.dist(color, C.WHITE) < C.dist(color, C.black)) ? C.black : C.white;
412
+ doneButton.label.color = tcolor;
413
+ return o_cgf(color);
414
+ };
415
+ doneButton.shape.cgf = cgf; // invokes shape.paint(cgf) !!
416
+ return actionCont;
417
+ }
418
+ setPlayerScore(plyr, score, rank) {
419
+ }
420
+ startGame(gameState) {
421
+ // All Tiles (& Meeple) are Dragable:
422
+ Tile.allTiles.forEach(tile => {
423
+ this.makeDragable(tile);
424
+ });
425
+ // this.stage.enableMouseOver(10);
426
+ this.scaleCont.addChild(this.overlayCont); // now at top of the list.
427
+ this.gamePlay.setNextPlayer(this.gamePlay.turnNumber > 0 ? this.gamePlay.turnNumber : 0);
428
+ }
429
+ makeDragable(tile) {
430
+ const dragger = this.dragger;
431
+ dragger.makeDragable(tile, this, this.dragFunc, this.dropFunc);
432
+ dragger.clickToDrag(tile, true); // also enable clickToDrag;
433
+ }
434
+ hexUnderObj(dragObj, legalOnly = true) {
435
+ return this.hexMap.hexUnderObj(dragObj, legalOnly);
436
+ }
437
+ dragContext;
438
+ dragFunc(tile, info) {
439
+ const hex = this.hexUnderObj(tile); // clickToDrag 'snaps' to non-original hex!
440
+ this.dragFunc0(tile, info, hex);
441
+ }
442
+ /** Table.dragFunc0 (Table.dragFunc to inject drag/start actions programatically)
443
+ * @param tile is being dragged
444
+ * @param info { first: boolean, mouse: MouseEvent }
445
+ * @param hex the Hex that tile is currently over (may be undefined or off map)
446
+ */
447
+ dragFunc0(tile, info, hex = this.hexUnderObj(tile)) {
448
+ let ctx = this.dragContext;
449
+ if (info?.first) {
450
+ if (ctx?.tile) {
451
+ // clickToDrag intercepting a drag in progress!
452
+ // mouse not over drag object! fix XY in call to dragTarget()
453
+ console.log(stime(this, `.dragFunc: OOPS! adjust XY on dragTarget`), ctx);
454
+ this.stopDragging(ctx.targetHex); // stop original drag
455
+ this.dragger.stopDrag(); // stop new drag; this.dropFunc(ctx.tile, ctx.info);
456
+ return;
457
+ }
458
+ const event = info.event?.nativeEvent;
459
+ tile.fromHex = tile.hex; // dragStart: set tile.fromHex when first move!
460
+ ctx = {
461
+ tile: tile,
462
+ targetHex: tile.fromHex,
463
+ lastShift: event?.shiftKey,
464
+ lastCtrl: event?.ctrlKey,
465
+ info: info,
466
+ nLegal: 0,
467
+ };
468
+ this.dragContext = ctx;
469
+ if (!tile.isDragable(ctx)) {
470
+ this.stopDragging(tile.fromHex); // just slide off this tile, no drag, no drop.
471
+ return;
472
+ }
473
+ this.dragStart(tile, ctx); // canBeMoved, isLegalTarget, tile.dragStart(ctx);
474
+ if (!ctx.tile)
475
+ return; // stopDragging() was invoked
476
+ }
477
+ this.checkShift(hex, ctx);
478
+ tile.dragFunc0(hex, ctx);
479
+ }
480
+ // invoke dragShift 'event' if shift state changes
481
+ checkShift(hex, ctx) {
482
+ const nativeEvent = ctx.info.event?.nativeEvent;
483
+ ctx.lastCtrl = nativeEvent?.ctrlKey;
484
+ // track shiftKey because we don't pass 'event' to isLegalTarget(hex)
485
+ const shiftKey = nativeEvent?.shiftKey;
486
+ if (shiftKey !== ctx.lastShift || (hex && ctx.targetHex !== hex)) {
487
+ ctx.lastShift = shiftKey;
488
+ // do shift-down/shift-up actions...
489
+ this.dragShift(ctx.tile, shiftKey, ctx); // was interesting for hexmarket
490
+ }
491
+ }
492
+ dragStart(tile, ctx) {
493
+ // press SHIFT to capture [recycle] opponent's Criminals or Tiles
494
+ const reason = tile.cantBeMovedBy(this.gamePlay.curPlayer, ctx);
495
+ if (reason) {
496
+ console.log(stime(this, `.dragStart: ${reason}: ${tile},`), 'ctx=', { ...ctx });
497
+ // this.logText(`${reason}: ${tile}`, 'Table.dragStart');
498
+ this.stopDragging();
499
+ }
500
+ else {
501
+ // mark legal targets for tile; SHIFT for all hexes, if payCost
502
+ tile.dragStart(ctx); // prepare for isLegalTarget
503
+ const countLegalHexes = (hex) => {
504
+ if (hex !== tile.hex && tile.isLegalTarget(hex, ctx)) {
505
+ hex.isLegal = true;
506
+ ctx.nLegal += 1;
507
+ }
508
+ };
509
+ tile.markLegal(this, countLegalHexes, ctx); // delegate to check each potential target
510
+ this.gamePlay.recycleHex.isLegal = tile.isLegalRecycle(ctx); // do not increment ctx.nLegal!
511
+ tile.moveTo(undefined); // notify source Hex, so it can scale; also triggers nextUnit !!
512
+ this.hexMap.update();
513
+ if (ctx.nLegal === 0) {
514
+ tile.noLegal();
515
+ if (!this.gamePlay.recycleHex.isLegal) {
516
+ this.stopDragging(); // actually, maybe let it drag, so we can see beneath...
517
+ }
518
+ }
519
+ }
520
+ }
521
+ /** state of shiftKey has changed during drag */
522
+ dragShift(tile, shiftKey, ctx) {
523
+ tile?.dragShift(shiftKey, ctx);
524
+ }
525
+ dropFunc(dobj, info, hex = this.hexUnderObj(dobj)) {
526
+ const tile = dobj;
527
+ tile.dropFunc0(hex, this.dragContext);
528
+ tile.markLegal(this); // hex => hex.isLegal = false;
529
+ this.gamePlay.recycleHex.isLegal = false;
530
+ this.dragContext.lastShift = undefined;
531
+ this.dragContext.tile = undefined; // mark not dragging
532
+ }
533
+ /** synthesize dragStart(tile), tile.dragFunc0(hex), dropFunc(tile); */
534
+ dragStartAndDrop(tile, toHex) {
535
+ if (!tile)
536
+ return; // C-q when no EventTile on eventHex
537
+ const info = { first: true }, hex = toHex;
538
+ this.dragFunc0(tile, info, tile.hex); // dragStart()
539
+ tile.dragFunc0(hex, this.dragContext);
540
+ this.dropFunc(tile, info, hex);
541
+ }
542
+ isDragging() { return this.dragContext?.tile !== undefined; }
543
+ /** Force this.dragger to drop the current drag object on given target Hex */
544
+ stopDragging(target = this.dragContext?.tile?.fromHex) {
545
+ //console.log(stime(this, `.stopDragging: dragObj=`), this.dragger.dragCont.getChildAt(0), {noMove, isDragging: this.isDragging()})
546
+ if (this.isDragging()) {
547
+ if (target)
548
+ this.dragContext.targetHex = target;
549
+ this.dragger.stopDrag(); // ---> dropFunc(this.dragContext.tile, info)
550
+ }
551
+ const data = this.dragger.getDragData(this.scaleCont);
552
+ if (data)
553
+ data.dragStopped = true;
554
+ }
555
+ /** Toggle dragging: dragTarget(target) OR stopDragging(targetHex)
556
+ * - attach supplied target to mouse-drag (default is eventHex.tile)
557
+ * @param target the DisplayObject being dragged
558
+ * @param xy offset from target to mouse pointer
559
+ */
560
+ dragTarget(target = this.gamePlay.recycleHex.tile, xy = { x: TP.hexRad / 2, y: TP.hexRad / 2 }) {
561
+ if (this.isDragging()) {
562
+ this.stopDragging(this.dragContext.targetHex); // drop and make move
563
+ }
564
+ else if (target) {
565
+ this.dragger.dragTarget(target, xy);
566
+ }
567
+ }
568
+ logCurPlayer(plyr) {
569
+ const history = this.gamePlay.history;
570
+ const tn = this.gamePlay.turnNumber;
571
+ const lm = history[0];
572
+ const prev = lm ? `${lm.Aname}${lm.ind}#${tn - 1}` : "";
573
+ const robo = plyr.useRobo ? AT.ansiText(['red', 'bold'], "robo") : "----";
574
+ const info = { turn: tn, plyr: plyr.Aname, prev, gamePlay: this.gamePlay, curPlayer: plyr };
575
+ console.log(stime(this, `.logCurPlayer --${robo}--`), info);
576
+ this.logTurn(`//${tn}: ${plyr.Aname}`);
577
+ }
578
+ showRedoUndoCount() {
579
+ this.undoText.text = `${this.gamePlay.undoRecs.length}`;
580
+ this.redoText.text = `${this.gamePlay.redoMoves.length}`;
581
+ }
582
+ showNextPlayer(log = true) {
583
+ let curPlayer = this.gamePlay.curPlayer; // after gamePlay.setNextPlayer()
584
+ if (log)
585
+ this.logCurPlayer(curPlayer);
586
+ this.showRedoUndoCount();
587
+ }
588
+ _tablePlanner;
589
+ get tablePlanner() {
590
+ return this._tablePlanner ||
591
+ (this._tablePlanner = new TablePlanner(this.gamePlay));
592
+ }
593
+ /**
594
+ * All manual moves feed through this (drop & redo)
595
+ * TablePlanner.logMove(); then dispatchEvent() --> gamePlay.doPlayerMove()
596
+ *
597
+ * New: let Ship (Drag & Drop) do this.
598
+ */
599
+ doTableMove(ihex) {
600
+ }
601
+ /** All moves (GUI & player) feed through this: */
602
+ moveStoneToHex(ihex, sc) {
603
+ // let hex = Hex.ofMap(ihex, this.hexMap)
604
+ // this.hexMap.showMark(hex)
605
+ // this.dispatchEvent(new HexEvent(S.add, hex, sc)) // -> GamePlay.playerMoveEvent(hex, sc)
606
+ }
607
+ /** default scaling-up value */
608
+ upscale = 1.5;
609
+ /** change cont.scale to given scale value. */
610
+ scaleUp(cont, scale = this.upscale) {
611
+ cont.scaleX = cont.scaleY = scale;
612
+ }
613
+ scaleParams = { initScale: .125, scale0: .05, scaleMax: 4, steps: 30, zscale: .20, };
614
+ scaleCont;
615
+ /** makeScaleableBack and setup scaleParams
616
+ * @param bindkeys true if there's a GUI/user/keyboard
617
+ */
618
+ makeScaleCont(bindKeys) {
619
+ /** scaleCont: a scalable background */
620
+ const scaleC = new ScaleableContainer2(this.stage, this.scaleParams);
621
+ this.dragger = new Dragger(scaleC);
622
+ if (!!scaleC.stage.canvas) {
623
+ // Special case of makeDragable; drag the parent of Dragger!
624
+ this.dragger.makeDragable(scaleC, scaleC, undefined, undefined, true); // THE case where not "useDragCont"
625
+ //this.scaleUp(Dragger.dragCont, 1.7); // Items being dragged appear larger!
626
+ }
627
+ if (bindKeys) {
628
+ this.bindKeysToScale(scaleC, "a", 436, 2);
629
+ KeyBinder.keyBinder.setKey('Space', { thisArg: this, func: () => this.dragTarget() });
630
+ KeyBinder.keyBinder.setKey('S-Space', { thisArg: this, func: () => this.dragTarget() });
631
+ KeyBinder.keyBinder.setKey('t', { thisArg: this, func: () => { this.toggleText(); } });
632
+ }
633
+ return scaleC;
634
+ }
635
+ /** put a Rectangle Shape at (0,0) with XYWH bounds as given */
636
+ setBackground(parent, bounds, bgColor = TP.bgColor) {
637
+ // specify an Area that is Dragable (mouse won't hit "empty" space)
638
+ const bgRect = new RectShape(bounds, bgColor, '');
639
+ bgRect.Aname = "BackgroundRect";
640
+ parent.addChildAt(bgRect, 0);
641
+ return bgRect;
642
+ }
643
+ zoom(z = 1.1) {
644
+ const stage = this.stage;
645
+ const pxy = { x: stage.mouseX / stage.scaleX, y: stage.mouseY / stage.scaleY };
646
+ this.scaleCont.setScale(this.scaleCont.scaleX * z, pxy);
647
+ // would require adjusting x,y offsets, so we just scale directly:
648
+ // TODO: teach ScaleableContainer to check scaleC.x,y before scroll-zooming.
649
+ // this.scaleCont.scaleX = this.scaleCont.scaleY = this.scaleCont.scaleX * z;
650
+ this.stage?.update();
651
+ }
652
+ pan(xy) {
653
+ this.scaleCont.x += xy.x;
654
+ this.scaleCont.y += xy.y;
655
+ this.stage?.update();
656
+ }
657
+ /**
658
+ * invoked before this.scaleC has been set
659
+ * @param scaleC same Container as this.scaleC
660
+ * @param char keybinding to set initial scale
661
+ * @param xos x-offset of scaleC in screen coords (pre-scale)
662
+ * @param yos y-offset of scaleC in screen coords (pre-scale)
663
+ * @param scale0 imitial scale [.5]
664
+ */
665
+ // bindKeysToScale('a', scaleC, 436, 0, .5)
666
+ bindKeysToScale(scaleC, char, xos, yos, scale0 = .5) {
667
+ const nsA = scale0;
668
+ const apt = { x: xos, y: yos };
669
+ let nsZ = 0.647; //
670
+ const zpt = { x: 120, y: 118 };
671
+ // set Keybindings to reset Scale:
672
+ /** save scale & offsets for later: */
673
+ const saveScaleZ = () => {
674
+ nsZ = scaleC.scaleX;
675
+ zpt.x = scaleC.x;
676
+ zpt.y = scaleC.y;
677
+ };
678
+ // xy is the fixed point, but is ignored because we set xy directly.
679
+ // sxy is the final xy offset, saved by saveScaleZ()
680
+ const setScaleXY = (ns, sxy = { x: 0, y: 0 }) => {
681
+ scaleC.setScale(ns);
682
+ //console.log({si, ns, xy, sxy, cw: this.canvas.width, iw: this.map_pixels.width})
683
+ scaleC.x = sxy.x;
684
+ scaleC.y = sxy.y;
685
+ this.stage.update();
686
+ };
687
+ const getOop = () => {
688
+ this.stage.getObjectsUnderPoint(500, 100, 1);
689
+ };
690
+ // Scale-setting keystrokes:
691
+ KeyBinder.keyBinder.setKey("a", { func: () => setScaleXY(nsA, apt) });
692
+ KeyBinder.keyBinder.setKey("z", { func: () => setScaleXY(nsZ, zpt) });
693
+ KeyBinder.keyBinder.setKey("x", { func: () => saveScaleZ() });
694
+ KeyBinder.keyBinder.setKey("p", { func: () => getOop(), thisArg: this });
695
+ KeyBinder.keyBinder.setKey('S-ArrowUp', { thisArg: this, func: this.zoom, argVal: 1.03 });
696
+ KeyBinder.keyBinder.setKey('S-ArrowDown', { thisArg: this, func: this.zoom, argVal: 1 / 1.03 });
697
+ KeyBinder.keyBinder.setKey('S-ArrowLeft', { thisArg: this, func: this.pan, argVal: { x: -10, y: 0 } });
698
+ KeyBinder.keyBinder.setKey('ArrowRight', { thisArg: this, func: this.pan, argVal: { x: 10, y: 0 } });
699
+ KeyBinder.keyBinder.setKey('ArrowLeft', { thisArg: this, func: this.pan, argVal: { x: -10, y: 0 } });
700
+ KeyBinder.keyBinder.setKey('S-ArrowRight', { thisArg: this, func: this.pan, argVal: { x: 10, y: 0 } });
701
+ KeyBinder.keyBinder.setKey('ArrowUp', { thisArg: this, func: this.pan, argVal: { x: 0, y: -10 } });
702
+ KeyBinder.keyBinder.setKey('ArrowDown', { thisArg: this, func: this.pan, argVal: { x: 0, y: 10 } });
703
+ KeyBinder.keyBinder.dispatchChar(char);
704
+ }
705
+ }
706
+ class ScaleableContainer2 extends ScaleableContainer {
707
+ }