@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/choosers.js +59 -0
- package/dist/counters.js +155 -0
- package/dist/game-play.js +455 -0
- package/dist/game-setup.js +280 -0
- package/dist/game-state.js +112 -0
- package/dist/hex-intfs.js +82 -0
- package/dist/hex.js +714 -0
- package/dist/image-loader.js +69 -0
- package/dist/meeple.js +152 -0
- package/dist/plan-proxy.js +33 -0
- package/dist/player-panel.js +125 -0
- package/dist/player.js +112 -0
- package/dist/scenario-parser.js +67 -0
- package/dist/shapes.js +304 -0
- package/dist/stream-writer.js +192 -0
- package/dist/table-params.js +48 -0
- package/dist/table.js +707 -0
- package/dist/text-log.js +53 -0
- package/dist/tile-source.js +115 -0
- package/dist/tile.js +312 -0
- package/dist/types.js +15 -0
- package/package.json +45 -0
package/dist/shapes.js
ADDED
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import { C, className } from "@thegraid/common-lib";
|
|
2
|
+
import { CenterText } from "@thegraid/easeljs-lib";
|
|
3
|
+
import { Container, Graphics, Shape, Text } from "@thegraid/easeljs-module";
|
|
4
|
+
import { H } from "./hex-intfs";
|
|
5
|
+
import { TP } from "./table-params";
|
|
6
|
+
export class C1 {
|
|
7
|
+
static GREY = 'grey';
|
|
8
|
+
static grey = 'grey';
|
|
9
|
+
static lightgrey2 = 'rgb(225,225,225)'; // needs to contrast with WHITE influence lines
|
|
10
|
+
static lightgrey_8 = 'rgb(225,225,225,.8)'; // needs to contrast with WHITE influence lines
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Usage: ??? [obsolete?]
|
|
14
|
+
* - ps = super.makeShape(); // ISA PaintableShape
|
|
15
|
+
* - ps.cgf = (color) => new CGF(color);
|
|
16
|
+
* - ...
|
|
17
|
+
* - ps.paint(red); --> ps.graphics = gf(red) --> new CG(red);
|
|
18
|
+
* -
|
|
19
|
+
* - const cgf: CGF = (color: string, g = new Graphics()) => {
|
|
20
|
+
* - return g.f(this.color).dc(0, 0, rad);
|
|
21
|
+
* - }
|
|
22
|
+
* - }
|
|
23
|
+
*/
|
|
24
|
+
export class PaintableShape extends Shape {
|
|
25
|
+
_cgf;
|
|
26
|
+
colorn;
|
|
27
|
+
/** initial/baseline Graphics, clone to create cgfGraphics */
|
|
28
|
+
g0;
|
|
29
|
+
/** previous/current Graphics that were rendered. (optimization... paint(color, true) to overrixe) */
|
|
30
|
+
cgfGraphics; // points to this.graphics after cgf runs.
|
|
31
|
+
/**
|
|
32
|
+
*
|
|
33
|
+
* @param _cgf Create Graphics Function
|
|
34
|
+
* @param colorn paint with this color
|
|
35
|
+
* @param g0 Graphics to clone (or create); used as baseline Graphics for each paint()
|
|
36
|
+
*/
|
|
37
|
+
constructor(_cgf, colorn = C.BLACK, g0) {
|
|
38
|
+
super();
|
|
39
|
+
this._cgf = _cgf;
|
|
40
|
+
this.colorn = colorn;
|
|
41
|
+
this.g0 = g0?.clone() ?? new Graphics(); // clone, because original is NOT immutable.
|
|
42
|
+
this.name = className(this);
|
|
43
|
+
}
|
|
44
|
+
updateCacheInPaint = true; // except for unusual cases
|
|
45
|
+
get cgf() { return this._cgf; }
|
|
46
|
+
/** set new cgf; and clear "previously rendered Graphics" */
|
|
47
|
+
set cgf(cgf) {
|
|
48
|
+
this._cgf = cgf;
|
|
49
|
+
if (this.cgfGraphics) {
|
|
50
|
+
this.paint(this.colorn, true);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/** render graphics from cgf. */
|
|
54
|
+
paint(colorn = this.colorn, force = false) {
|
|
55
|
+
if (force || this.graphics !== this.cgfGraphics || this.colorn !== colorn) {
|
|
56
|
+
// need to repaint, even if same color:
|
|
57
|
+
this.graphics = this.g0.clone(); // reset to initial Graphics.
|
|
58
|
+
this.graphics = this.cgfGraphics = this.cgf(this.colorn = colorn); // apply this.cgf(color)
|
|
59
|
+
if (this.updateCacheInPaint && this.cacheID)
|
|
60
|
+
this.updateCache();
|
|
61
|
+
}
|
|
62
|
+
return this.graphics;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* The colored PaintableShape that fills a Hex.
|
|
67
|
+
* @param radius in call to drawPolyStar()
|
|
68
|
+
*/
|
|
69
|
+
export class HexShape extends PaintableShape {
|
|
70
|
+
radius;
|
|
71
|
+
tilt;
|
|
72
|
+
constructor(radius = TP.hexRad, tilt = TP.useEwTopo ? 30 : 0) {
|
|
73
|
+
super((fillc) => this.hscgf(fillc));
|
|
74
|
+
this.radius = radius;
|
|
75
|
+
this.tilt = tilt;
|
|
76
|
+
this.setHexBounds(); // Assert radius & tilt are readonly, so bounds never changes!
|
|
77
|
+
}
|
|
78
|
+
setHexBounds(r = this.radius, tilt = this.tilt) {
|
|
79
|
+
const b = H.hexBounds(r, tilt);
|
|
80
|
+
this.setBounds(b.x, b.y, b.width, b.height);
|
|
81
|
+
}
|
|
82
|
+
setCacheID() {
|
|
83
|
+
const b = this.getBounds(); // Bounds are set
|
|
84
|
+
this.cache(b.x, b.y, b.width, b.height);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Draw a Hexagon 1/60th inside the given radius.
|
|
88
|
+
* overrides should include call to setHexBounds(radius, angle)
|
|
89
|
+
* or in other way setBounds().
|
|
90
|
+
*/
|
|
91
|
+
hscgf(color, g0 = this.graphics) {
|
|
92
|
+
return g0.f(color).dp(0, 0, Math.floor(this.radius * 59 / 60), 6, 0, this.tilt); // 30 or 0
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
export class EllipseShape extends PaintableShape {
|
|
96
|
+
fillc;
|
|
97
|
+
radx;
|
|
98
|
+
rady;
|
|
99
|
+
strokec;
|
|
100
|
+
/**
|
|
101
|
+
* ellipse centered on (0,0), axis is NS/EW, rotate after.
|
|
102
|
+
* @param radx radius in x dir
|
|
103
|
+
* @param rady radisu in y dir
|
|
104
|
+
* retain g0, to use as baseline Graphics for each paint()
|
|
105
|
+
*/
|
|
106
|
+
constructor(fillc = C.white, radx = 30, rady = 30, strokec = C.black, g0) {
|
|
107
|
+
super((fillc) => this.cscgf(fillc), strokec, g0);
|
|
108
|
+
this.fillc = fillc;
|
|
109
|
+
this.radx = radx;
|
|
110
|
+
this.rady = rady;
|
|
111
|
+
this.strokec = strokec;
|
|
112
|
+
this._cgf = this.cscgf; // overwrite to remove indirection...
|
|
113
|
+
this.paint(fillc);
|
|
114
|
+
}
|
|
115
|
+
cscgf(fillc, g = this.g0.clone()) {
|
|
116
|
+
((this.fillc = fillc) ? g.f(fillc) : g.ef());
|
|
117
|
+
(this.strokec ? g.s(this.strokec) : g.es());
|
|
118
|
+
g.de(-this.radx, -this.rady, 2 * this.radx, 2 * this.rady); // easlejs can determine Bounds of Ellipse
|
|
119
|
+
return g;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Circle centered on (0,0)
|
|
124
|
+
* @param rad radius
|
|
125
|
+
* retain g0, to use as baseline Graphics for each paint()
|
|
126
|
+
*/
|
|
127
|
+
export class CircleShape extends EllipseShape {
|
|
128
|
+
constructor(fillc = C.white, rad = 30, strokec = C.black, g0) {
|
|
129
|
+
super(fillc, rad, rad, strokec, g0);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
export class PolyShape extends PaintableShape {
|
|
133
|
+
nsides;
|
|
134
|
+
tilt;
|
|
135
|
+
fillc;
|
|
136
|
+
rad;
|
|
137
|
+
strokec;
|
|
138
|
+
constructor(nsides = 4, tilt = 0, fillc = C.white, rad = 30, strokec = C.black, g0) {
|
|
139
|
+
super((fillc) => this.pscgf(fillc), fillc, g0);
|
|
140
|
+
this.nsides = nsides;
|
|
141
|
+
this.tilt = tilt;
|
|
142
|
+
this.fillc = fillc;
|
|
143
|
+
this.rad = rad;
|
|
144
|
+
this.strokec = strokec;
|
|
145
|
+
this._cgf = this.pscgf;
|
|
146
|
+
this.paint(fillc);
|
|
147
|
+
}
|
|
148
|
+
pscgf(fillc, g = this.g0?.clone() ?? new Graphics()) {
|
|
149
|
+
((this.fillc = fillc) ? g.f(fillc) : g.ef());
|
|
150
|
+
(this.strokec ? g.s(this.strokec) : g.es());
|
|
151
|
+
g.dp(0, 0, this.rad, this.nsides, 0, this.tilt * H.degToRadians);
|
|
152
|
+
return g;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
export class RectShape extends PaintableShape {
|
|
156
|
+
fillc;
|
|
157
|
+
strokec;
|
|
158
|
+
static rectWHXY(w, h, x = -w / 2, y = -h / 2, g0 = new Graphics()) {
|
|
159
|
+
return g0.dr(x, y, w, h);
|
|
160
|
+
}
|
|
161
|
+
static rectWHXYr(w, h, x = -w / 2, y = -h / 2, r = 0, g0 = new Graphics()) {
|
|
162
|
+
return g0.rr(x, y, w, h, r);
|
|
163
|
+
}
|
|
164
|
+
/** draw rectangle suitable for given Text; with border, textAlign. */
|
|
165
|
+
static rectText(t, fs, b, align = (t instanceof Text) ? t.textAlign : 'center', g0 = new Graphics()) {
|
|
166
|
+
const txt = (t instanceof Text) ? t : new CenterText(t, fs ?? 30);
|
|
167
|
+
txt.textAlign = align;
|
|
168
|
+
if (txt.text === undefined)
|
|
169
|
+
return g0; // or RectShape.rectWHXY(0,0,0,0); ??
|
|
170
|
+
if (fs === undefined)
|
|
171
|
+
fs = txt.getMeasuredHeight();
|
|
172
|
+
if (b === undefined)
|
|
173
|
+
b = fs * .1;
|
|
174
|
+
const txtw = txt.getMeasuredWidth(), w = b + txtw + b, h = b + fs + b;
|
|
175
|
+
const x = (align == 'right') ? w - b : (align === 'left') ? -b : w / 2;
|
|
176
|
+
return RectShape.rectWHXY(w, h, -x, -h / 2, g0);
|
|
177
|
+
}
|
|
178
|
+
rect;
|
|
179
|
+
rc = 0;
|
|
180
|
+
constructor({ x = 0, y = 0, w = 30, h = 30, r = 0 }, fillc = C.white, strokec = C.black, g0) {
|
|
181
|
+
super((fillc) => this.rscgf(fillc), fillc, g0);
|
|
182
|
+
this.fillc = fillc;
|
|
183
|
+
this.strokec = strokec;
|
|
184
|
+
this._cgf = this.rscgf;
|
|
185
|
+
this.rect = { x, y, w, h };
|
|
186
|
+
this.setBounds(x, y, w, h);
|
|
187
|
+
this.rc = r;
|
|
188
|
+
this.g0 = g0?.clone() ?? new Graphics();
|
|
189
|
+
this.paint(fillc, true); // this.graphics = rscgf(...)
|
|
190
|
+
}
|
|
191
|
+
rscgf(fillc, g = this.g0?.clone() ?? new Graphics()) {
|
|
192
|
+
const { x, y, w, h } = this.rect;
|
|
193
|
+
(fillc ? g.f(fillc) : g.ef());
|
|
194
|
+
(this.strokec ? g.s(this.strokec) : g.es());
|
|
195
|
+
if (this.rc === 0) {
|
|
196
|
+
g.dr(x ?? 0, y ?? 0, w ?? 30, h ?? 30);
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
g.rr(x ?? 0, y ?? 0, w ?? 30, h ?? 30, this.rc);
|
|
200
|
+
}
|
|
201
|
+
return g;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
/** from hextowns, with translucent center. */
|
|
205
|
+
export class TileShape extends HexShape {
|
|
206
|
+
static fillColor = C1.lightgrey_8; // 'rgba(200,200,200,.8)'
|
|
207
|
+
constructor(radius, tilt) {
|
|
208
|
+
super(radius, tilt); // sets Bounnds & this.cgf
|
|
209
|
+
this.cgf = this.tscgf;
|
|
210
|
+
}
|
|
211
|
+
replaceDisk(colorn, r2 = this.radius) {
|
|
212
|
+
if (!this.cacheID)
|
|
213
|
+
this.setCacheID();
|
|
214
|
+
else
|
|
215
|
+
this.updateCache(); // write curent graphics to cache
|
|
216
|
+
const g = this.graphics;
|
|
217
|
+
g.c().f(C.BLACK).dc(0, 0, r2); // bits to remove
|
|
218
|
+
this.updateCache("destination-out"); // remove disk from solid hexagon
|
|
219
|
+
g.c().f(colorn).dc(0, 0, r2); // fill with translucent disk
|
|
220
|
+
this.updateCache("source-over"); // update with new disk
|
|
221
|
+
return g;
|
|
222
|
+
}
|
|
223
|
+
bgColor = C.nameToRgbaString(C.WHITE, .8);
|
|
224
|
+
/** colored HexShape filled with very-lightgrey disk: */
|
|
225
|
+
tscgf(colorn, g0 = this.cgfGraphics?.clone() ?? new Graphics(), super_cgf = (color) => new Graphics()) {
|
|
226
|
+
// HexShape.cgf(rgba(C.WHITE, .8))
|
|
227
|
+
const g = this.graphics = super_cgf.call(this, this.bgColor); // paint HexShape(White)
|
|
228
|
+
const fillColor = C.nameToRgbaString(colorn, .8);
|
|
229
|
+
this.replaceDisk(fillColor, this.radius * H.sqrt3_2 * (55 / 60));
|
|
230
|
+
return this.graphics = g;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
export class LegalMark extends Shape {
|
|
234
|
+
hex2;
|
|
235
|
+
setOnHex(hex) {
|
|
236
|
+
this.hex2 = hex;
|
|
237
|
+
const parent = hex.mapCont.markCont;
|
|
238
|
+
this.graphics.f(C.legalGreen).dc(0, 0, TP.hexRad / 2);
|
|
239
|
+
hex.cont.parent.localToLocal(hex.x, hex.y, parent, this);
|
|
240
|
+
this.hitArea = hex.hexShape; // legal mark is used for hexUnderObject, so need to cover whole hex.
|
|
241
|
+
this.mouseEnabled = true;
|
|
242
|
+
this.visible = false;
|
|
243
|
+
parent.addChild(this);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
export class UtilButton extends Container {
|
|
247
|
+
fontSize;
|
|
248
|
+
textColor;
|
|
249
|
+
blocked = false;
|
|
250
|
+
shape;
|
|
251
|
+
label;
|
|
252
|
+
get label_text() { return this.label.text; }
|
|
253
|
+
set label_text(t) {
|
|
254
|
+
this.label.text = t;
|
|
255
|
+
this.paint(undefined, true);
|
|
256
|
+
}
|
|
257
|
+
constructor(color, text, fontSize = 30, textColor = C.black, cgf) {
|
|
258
|
+
super();
|
|
259
|
+
this.fontSize = fontSize;
|
|
260
|
+
this.textColor = textColor;
|
|
261
|
+
this.label = new CenterText(text, fontSize, textColor);
|
|
262
|
+
this.shape = new PaintableShape(cgf ?? ((c) => this.ubcsf(c)));
|
|
263
|
+
this.shape.paint(color);
|
|
264
|
+
this.addChild(this.shape, this.label);
|
|
265
|
+
}
|
|
266
|
+
ubcsf(color, g = new Graphics()) {
|
|
267
|
+
return RectShape.rectText(this.label.text, this.fontSize, this.fontSize * .3, this.label.textAlign, g.f(color));
|
|
268
|
+
}
|
|
269
|
+
paint(color = this.shape.colorn, force = false) {
|
|
270
|
+
return this.shape.paint(color, force);
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Repaint the stage with button visible or not.
|
|
274
|
+
*
|
|
275
|
+
* Allow Chrome to finish stage.update before proceeding with afterUpdate().
|
|
276
|
+
*
|
|
277
|
+
* Other code can watch this.blocked; then call updateWait(false) to reset.
|
|
278
|
+
* @param hide true to hide and disable the turnButton
|
|
279
|
+
* @param afterUpdate callback ('drawend') when stage.update is done [none]
|
|
280
|
+
* @param scope thisArg for afterUpdate [this TurnButton]
|
|
281
|
+
* @deprecated use easeljs-lib afterUpdate(container, function)
|
|
282
|
+
*/
|
|
283
|
+
updateWait(hide, afterUpdate, scope = this) {
|
|
284
|
+
this.blocked = hide;
|
|
285
|
+
this.visible = this.mouseEnabled = !hide;
|
|
286
|
+
// using @thegraid/easeljs-module@^1.1.8: on(once=true) will now 'just work'
|
|
287
|
+
afterUpdate && this.stage.on('drawend', afterUpdate, scope, true);
|
|
288
|
+
this.stage.update();
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
export class EdgeShape extends Shape {
|
|
292
|
+
color;
|
|
293
|
+
hex;
|
|
294
|
+
dir;
|
|
295
|
+
constructor(color, hex, dir, parent) {
|
|
296
|
+
super();
|
|
297
|
+
this.color = color;
|
|
298
|
+
this.hex = hex;
|
|
299
|
+
this.dir = dir;
|
|
300
|
+
this.reset();
|
|
301
|
+
parent.addChild(this);
|
|
302
|
+
}
|
|
303
|
+
reset(color = this.color) { this.graphics.c().ss(12, 'round', 'round').s(color); }
|
|
304
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { AT, S, stime } from "@thegraid/common-lib";
|
|
2
|
+
class FileBase {
|
|
3
|
+
name;
|
|
4
|
+
buttonId;
|
|
5
|
+
constructor(name = 'logFile', buttonId = "fsOpenFileButton") {
|
|
6
|
+
this.name = name;
|
|
7
|
+
this.buttonId = buttonId;
|
|
8
|
+
}
|
|
9
|
+
/** FileHandle obtained from FilePicker Button. */
|
|
10
|
+
fileHandle;
|
|
11
|
+
/** stime ident string in 'red' */
|
|
12
|
+
ident(id, color = 'red') { return AT.ansiText([color], `.${id}:`); }
|
|
13
|
+
/** multi-purpose picker button: (callback arg-type changes)
|
|
14
|
+
*
|
|
15
|
+
* @param method 'showOpenFilePicker' | 'showSaveFilePicker' | 'showDirectoryPicker'
|
|
16
|
+
* @param options from "wicg-file-system-access":
|
|
17
|
+
* - OpenFilePickerOptions { multiple?: boolean }
|
|
18
|
+
* - SaveFilePickerOptions { suggestedName?: string }
|
|
19
|
+
* - DirectoryPickerOptions {}
|
|
20
|
+
* @param cb returns the fileHandle/fileHandleAry
|
|
21
|
+
* @param inText set innerText of button ['OpenFile', 'SaveFile', 'Directory'] based on method
|
|
22
|
+
*/
|
|
23
|
+
setButton(method, options, cb, inText = method.substring(4, method.length - 6)) {
|
|
24
|
+
const picker = window[method]; // showSaveFilePicker showDirectoryPicker
|
|
25
|
+
const fsOpenButton = document.getElementById(this.buttonId); // must exist!
|
|
26
|
+
fsOpenButton.innerText = inText;
|
|
27
|
+
fsOpenButton.onclick = () => {
|
|
28
|
+
picker(options).then((value) => cb(value), (rej) => console.warn(`${method} failed: `, rej));
|
|
29
|
+
};
|
|
30
|
+
return fsOpenButton;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Supply a button-id in HTML, when user clicks the file is opened for write-append.
|
|
35
|
+
*
|
|
36
|
+
* Other code can: new LogWriter().writeLine('first line...')
|
|
37
|
+
* to queue writes before user clicks.
|
|
38
|
+
*
|
|
39
|
+
* file is flushed/closed & re-opened after every writeLine.
|
|
40
|
+
* (so log is already saved if browser crashes...)
|
|
41
|
+
*/
|
|
42
|
+
export class LogWriter extends FileBase {
|
|
43
|
+
atZero;
|
|
44
|
+
atEnd;
|
|
45
|
+
/** when fulfilled, value is a WriteableFileStream; from createWriteable(). */
|
|
46
|
+
streamPromise;
|
|
47
|
+
/** WriteableFileStream Promise that is fulfilled when stream is open & ready for write */
|
|
48
|
+
async openWriteStream(fileHandle = this.fileHandle, options = { keepExistingData: true }) {
|
|
49
|
+
const promise = fileHandle.createWritable(options), thus = this;
|
|
50
|
+
this.streamPromise = promise;
|
|
51
|
+
const x = promise.then(() => this.writeBacklog(thus));
|
|
52
|
+
return promise;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* @param name suggested name for write file
|
|
56
|
+
* @param atEnd insert at end-of-file; but remove before writeLine. [\n]
|
|
57
|
+
* @param buttonId DOM id of button to click to bring up FilePicker
|
|
58
|
+
*/
|
|
59
|
+
constructor(name = 'logFile', atZero = '', atEnd = '\n', buttonId = "fsOpenFileButton") {
|
|
60
|
+
super(name, buttonId);
|
|
61
|
+
this.atZero = atZero;
|
|
62
|
+
this.atEnd = atEnd;
|
|
63
|
+
this.setButtonToSaveLog();
|
|
64
|
+
}
|
|
65
|
+
setButtonToSaveLog(name = this.name) {
|
|
66
|
+
const options = {
|
|
67
|
+
id: 'logWriter',
|
|
68
|
+
startIn: 'downloads',
|
|
69
|
+
suggestedName: name,
|
|
70
|
+
types: [{
|
|
71
|
+
description: 'Text/Javascript Files',
|
|
72
|
+
accept: { 'text/plain': ['.txt', '.js'], },
|
|
73
|
+
},],
|
|
74
|
+
};
|
|
75
|
+
// console.log(stime(this, `.new LogWriter:`), { file: this.fileHandle })
|
|
76
|
+
// Note return type changes: [FileHandle], [DirHandle], FileHandle
|
|
77
|
+
this.setButton('showSaveFilePicker', options, (fileHandle) => {
|
|
78
|
+
this.fileHandle = fileHandle;
|
|
79
|
+
this.fileName = fileHandle.name;
|
|
80
|
+
console.log(stime(this, `${this.ident('FilePicker')}.picked:`), fileHandle);
|
|
81
|
+
this.openWriteStream(fileHandle);
|
|
82
|
+
}, 'SaveLog');
|
|
83
|
+
}
|
|
84
|
+
fileName;
|
|
85
|
+
xfileName; // retain last fileName when file is closed
|
|
86
|
+
backlog = [];
|
|
87
|
+
writeLine(text = '') {
|
|
88
|
+
const wasNoBacklog = (this.backlog.length === 0);
|
|
89
|
+
this.backlog.push(`${text}\n`);
|
|
90
|
+
if (wasNoBacklog && this.streamPromise) {
|
|
91
|
+
this.writeBacklog(this); // try write new backlog.
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
showBacklog() {
|
|
95
|
+
console.log(stime(this, `.showBacklog:\n`));
|
|
96
|
+
const backlog = this.backlog.join('');
|
|
97
|
+
console.log(backlog);
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* called when openWriteStream has fulfilled streamPromise with a new writeableStream.
|
|
101
|
+
*
|
|
102
|
+
* or when application invokes writeline when stream is already open.
|
|
103
|
+
*/
|
|
104
|
+
async writeBacklog(thus = this) {
|
|
105
|
+
//console.log(stime(this, ident), `Backlog:`, this.backlog.length, this.backlog)
|
|
106
|
+
if (thus.backlog.length > 0)
|
|
107
|
+
try {
|
|
108
|
+
const stream = await thus.streamPromise; // indicates writeable is ready
|
|
109
|
+
const fileHandle = await thus.fileHandle.getFile();
|
|
110
|
+
const size = fileHandle.size;
|
|
111
|
+
const line0 = (size === 0) ? this.atZero : '';
|
|
112
|
+
await stream.seek(Math.max(0, size - this.atEnd.length));
|
|
113
|
+
const nlines = thus.backlog.length;
|
|
114
|
+
const lines = `${line0}${this.backlog.slice(0, nlines).join('')}${this.atEnd}`; // shift all lines; commit to writing
|
|
115
|
+
await stream.write({ type: 'write', data: lines }); // write to tmp store
|
|
116
|
+
await stream.close(); // flush to file system.
|
|
117
|
+
thus.backlog.splice(0, nlines); // remove from backlog
|
|
118
|
+
thus.streamPromise = thus.openWriteStream(); // begin open new stream (streamPromise will be fullfiled)
|
|
119
|
+
}
|
|
120
|
+
catch (err) {
|
|
121
|
+
console.warn(stime(thus, thus.ident('writeBacklog')), `failed:`, err);
|
|
122
|
+
throw err;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
async closeFile() {
|
|
126
|
+
if (this.streamPromise)
|
|
127
|
+
try {
|
|
128
|
+
const stream = await this.streamPromise;
|
|
129
|
+
const promise = stream.close();
|
|
130
|
+
this.streamPromise = undefined;
|
|
131
|
+
this.xfileName = this.fileName;
|
|
132
|
+
this.fileName = undefined;
|
|
133
|
+
return promise;
|
|
134
|
+
}
|
|
135
|
+
catch (err) {
|
|
136
|
+
console.warn(stime(this, `.closeFile failed:`), err);
|
|
137
|
+
throw err;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
pickLogFile(name = this.name) {
|
|
141
|
+
const fsOpenButton = document.getElementById(this.buttonId);
|
|
142
|
+
this.setButtonToSaveLog(name);
|
|
143
|
+
fsOpenButton.click();
|
|
144
|
+
}
|
|
145
|
+
/** Old technique: creates a *new* file each time it saves/downloads the given Blob(text) */
|
|
146
|
+
downloadViaHiddenButton(name, text) {
|
|
147
|
+
const a = document.createElement('a');
|
|
148
|
+
let blob = new Blob([text], { type: "text/plain;charset=utf-8" });
|
|
149
|
+
a.href = URL.createObjectURL(blob);
|
|
150
|
+
a.download = name;
|
|
151
|
+
a.addEventListener(S.click, (e) => {
|
|
152
|
+
setTimeout(() => URL.revokeObjectURL(a.href), 3 * 1000); // is there no completion callback?
|
|
153
|
+
});
|
|
154
|
+
a.click();
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
export class LogReader extends FileBase {
|
|
158
|
+
constructor(name = 'logFile', buttonId = "fsReadFileButton") {
|
|
159
|
+
super(name, buttonId);
|
|
160
|
+
}
|
|
161
|
+
pickFileToRead() {
|
|
162
|
+
const fsOpenButton = document.getElementById(this.buttonId); // ASSERT: button element exists.
|
|
163
|
+
let fileReadPromise = this.setButtonToReadFile();
|
|
164
|
+
fsOpenButton.click();
|
|
165
|
+
return fileReadPromise;
|
|
166
|
+
}
|
|
167
|
+
/** OpenFilePickerOptions:
|
|
168
|
+
* - types?: FilePickerAcceptType[] | undefined;
|
|
169
|
+
* - excludeAcceptAllOption?: boolean | undefined;
|
|
170
|
+
* - multiple?: false;
|
|
171
|
+
*/
|
|
172
|
+
setButtonToReadFile() {
|
|
173
|
+
return new Promise((fulfill => {
|
|
174
|
+
this.setButton('showOpenFilePicker', {}, ([fileHandle]) => {
|
|
175
|
+
this.fileHandle = fileHandle;
|
|
176
|
+
fulfill(this.fileHandle.getFile());
|
|
177
|
+
}, 'LoadFile');
|
|
178
|
+
}));
|
|
179
|
+
}
|
|
180
|
+
// async readPickedFile(fileReadPromise: File | Promise<File> = this.pickFileToRead()) {
|
|
181
|
+
// return this.readFile(await fileReadPromise)
|
|
182
|
+
// }
|
|
183
|
+
async readFile(file) {
|
|
184
|
+
return new Promise((fulfill) => {
|
|
185
|
+
let fileReader = new FileReader();
|
|
186
|
+
fileReader.onload = () => {
|
|
187
|
+
fulfill(fileReader.result);
|
|
188
|
+
};
|
|
189
|
+
fileReader.readAsText(file); // , encoding=utf-8 => void!
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export const playerColors = ['b', 'w']; // Player Colors!
|
|
2
|
+
export const playerColorsC = ['b', 'w', 'c']; // Player Colors + Criminal!
|
|
3
|
+
export const playerColor0 = playerColors[0];
|
|
4
|
+
export const playerColor1 = playerColors[1];
|
|
5
|
+
export const playerColor2 = playerColorsC[2];
|
|
6
|
+
export function otherColor(color) { return color === playerColor0 ? playerColor1 : playerColor0; }
|
|
7
|
+
export function playerColorRecord(b, w, c) { return { b, w, c }; }
|
|
8
|
+
;
|
|
9
|
+
export function playerColorRecordF(f) { return playerColorRecord(f(playerColor0), f(playerColor1), f(playerColor2)); }
|
|
10
|
+
export function buildURL(scheme = 'wss', host = TP.ghost, domain = TP.gdomain, port = TP.gport, path = '') {
|
|
11
|
+
return `${scheme}://${host}.${domain}:${port}${path}`;
|
|
12
|
+
}
|
|
13
|
+
export class TP {
|
|
14
|
+
static colorScheme = playerColorRecordF(n => n);
|
|
15
|
+
static useEwTopo = true; // spiral districts require useEwTopo === true
|
|
16
|
+
static cacheTiles = 2;
|
|
17
|
+
static snapToPixel = true;
|
|
18
|
+
static textLogLines = 13;
|
|
19
|
+
static log = 0; // log level; see also: GamePlay.ll(n)
|
|
20
|
+
static numPlayers = 2;
|
|
21
|
+
static maxPlayers = 6;
|
|
22
|
+
static mapRows = 7; /// standard: 6 (AnkhMap)
|
|
23
|
+
static mapCols = 12; /// standard: 15
|
|
24
|
+
static nHexes = 6;
|
|
25
|
+
static mHexes = 1;
|
|
26
|
+
static playerRGBcolors = []; // filled by Player.initialize()
|
|
27
|
+
static autoEvent = 2000;
|
|
28
|
+
// timeout: see also 'autoEvent'
|
|
29
|
+
static moveDwell = 600;
|
|
30
|
+
static flashDwell = 500;
|
|
31
|
+
static flipDwell = 200; // chooseStartPlayer dwell between each card flip
|
|
32
|
+
static bgColor = 'rgba(155, 100, 150, .3)';
|
|
33
|
+
static bgRect = { x: -2400, y: -1000, w: 8000, h: 5000 };
|
|
34
|
+
static ghost = 'game7'; // game-setup.network()
|
|
35
|
+
static gdomain = 'thegraid.com';
|
|
36
|
+
static gport = 8447;
|
|
37
|
+
static networkUrl = 'wss://game7.thegraid.com:8447'; // URL to cgserver (wspbserver)
|
|
38
|
+
static networkGroup = 'citymap:game1';
|
|
39
|
+
static vpToWin = 20;
|
|
40
|
+
static roboDrawTile = 1.0; // Bias toward draw Tile
|
|
41
|
+
static trapNotDropTarget = true; // warn & alert when D&D to non-DropTarget
|
|
42
|
+
static hexRad = 60;
|
|
43
|
+
static meepleRad = 45;
|
|
44
|
+
static meepleY0 = 15;
|
|
45
|
+
// for AI control:
|
|
46
|
+
static maxPlys = 3;
|
|
47
|
+
static maxBreadth = 3;
|
|
48
|
+
}
|