@mideind/netskrafl-react 1.0.0-beta.1
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/.eslintignore +8 -0
- package/.eslintrc.json +13 -0
- package/README.md +63 -0
- package/dist/cjs/index.css +6837 -0
- package/dist/cjs/index.js +3046 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/esm/index.css +6837 -0
- package/dist/esm/index.js +3046 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/types.d.ts +41 -0
- package/package.json +63 -0
- package/rollup.config.js +60 -0
- package/src/components/index.ts +2 -0
- package/src/components/netskrafl/Netskrafl.stories.tsx +66 -0
- package/src/components/netskrafl/Netskrafl.tsx +135 -0
- package/src/components/netskrafl/Netskrafl.types.ts +7 -0
- package/src/components/netskrafl/index.ts +2 -0
- package/src/css/fonts.css +4 -0
- package/src/css/glyphs.css +224 -0
- package/src/css/skrafl-explo.css +6616 -0
- package/src/fonts/glyphicons-regular.eot +0 -0
- package/src/fonts/glyphicons-regular.ttf +0 -0
- package/src/fonts/glyphicons-regular.woff +0 -0
- package/src/index.ts +2 -0
- package/src/messages/messages.json +1576 -0
- package/src/mithril/actions.ts +319 -0
- package/src/mithril/bag.ts +65 -0
- package/src/mithril/bestdisplay.ts +74 -0
- package/src/mithril/blankdialog.ts +94 -0
- package/src/mithril/board.ts +336 -0
- package/src/mithril/buttons.ts +303 -0
- package/src/mithril/challengedialog.ts +186 -0
- package/src/mithril/channel.ts +162 -0
- package/src/mithril/chat.ts +228 -0
- package/src/mithril/components.ts +496 -0
- package/src/mithril/dragdrop.ts +219 -0
- package/src/mithril/elopage.ts +180 -0
- package/src/mithril/friend.ts +227 -0
- package/src/mithril/game.ts +1378 -0
- package/src/mithril/gameview.ts +111 -0
- package/src/mithril/globalstate.ts +33 -0
- package/src/mithril/i18n.ts +186 -0
- package/src/mithril/localstorage.ts +133 -0
- package/src/mithril/login.ts +122 -0
- package/src/mithril/logo.ts +270 -0
- package/src/mithril/main.ts +737 -0
- package/src/mithril/mithril.ts +29 -0
- package/src/mithril/model.ts +817 -0
- package/src/mithril/movelistitem.ts +226 -0
- package/src/mithril/page.ts +852 -0
- package/src/mithril/playername.ts +91 -0
- package/src/mithril/promodialog.ts +82 -0
- package/src/mithril/recentlist.ts +148 -0
- package/src/mithril/request.ts +52 -0
- package/src/mithril/review.ts +634 -0
- package/src/mithril/rightcolumn.ts +398 -0
- package/src/mithril/searchbutton.ts +118 -0
- package/src/mithril/statsdisplay.ts +109 -0
- package/src/mithril/tabs.ts +169 -0
- package/src/mithril/tile.ts +145 -0
- package/src/mithril/twoletter.ts +76 -0
- package/src/mithril/types.ts +379 -0
- package/src/mithril/userinfodialog.ts +171 -0
- package/src/mithril/util.ts +304 -0
- package/src/mithril/wait.ts +246 -0
- package/src/mithril/wordcheck.ts +102 -0
- package/tsconfig.json +28 -0
- package/vite.config.ts +12 -0
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
/*
|
|
2
|
+
|
|
3
|
+
Board.ts
|
|
4
|
+
|
|
5
|
+
Board, Tile and Rack components
|
|
6
|
+
|
|
7
|
+
Copyright (C) 2024 Miðeind ehf.
|
|
8
|
+
Author: Vilhjalmur Thorsteinsson
|
|
9
|
+
|
|
10
|
+
The Creative Commons Attribution-NonCommercial 4.0
|
|
11
|
+
International Public License (CC-BY-NC 4.0) applies to this software.
|
|
12
|
+
For further information, see https://github.com/mideind/Netskrafl
|
|
13
|
+
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { ERROR_MESSAGES, IGame, IView, RACK_SIZE } from "./types";
|
|
17
|
+
import { addPinchZoom, glyph } from "./util";
|
|
18
|
+
import { Component, ComponentFunc, m, VnodeAttrs, VnodeChildren } from "./mithril";
|
|
19
|
+
import { Buttons, ButtonsLeftOfRack } from "./buttons";
|
|
20
|
+
import { Tile } from "./tile";
|
|
21
|
+
import { mt, ts } from "./i18n";
|
|
22
|
+
|
|
23
|
+
const GameOver: Component<{ game: IGame }> = {
|
|
24
|
+
view: (vnode) => {
|
|
25
|
+
// Show message at end of game, either congratulating a win or
|
|
26
|
+
// solemnly informing the player that the game is over
|
|
27
|
+
const { game } = vnode.attrs;
|
|
28
|
+
if (game.congratulate) {
|
|
29
|
+
return m("div", { id: "congrats" },
|
|
30
|
+
[
|
|
31
|
+
glyph("bookmark"),
|
|
32
|
+
" ",
|
|
33
|
+
mt("strong", "Til hamingju með sigurinn!")
|
|
34
|
+
]
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
if (game.over) {
|
|
38
|
+
return m("div", { id: "gameover" },
|
|
39
|
+
[
|
|
40
|
+
glyph("info-sign"),
|
|
41
|
+
" ",
|
|
42
|
+
mt("strong", "Viðureigninni er lokið")
|
|
43
|
+
]
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const Errors: Component<{ game: IGame }> = {
|
|
51
|
+
view: (vnode) => {
|
|
52
|
+
// Error messages, selectively displayed
|
|
53
|
+
const { game } = vnode.attrs;
|
|
54
|
+
const err = game.currentError || "";
|
|
55
|
+
if (err && err in ERROR_MESSAGES) {
|
|
56
|
+
const msg: string = game.currentMessage || "";
|
|
57
|
+
const txt: string = ts(ERROR_MESSAGES[err]);
|
|
58
|
+
const wix = txt.indexOf("{word}");
|
|
59
|
+
let children: VnodeChildren[];
|
|
60
|
+
if (wix >= 0) {
|
|
61
|
+
// Found {word} macro: create three child nodes
|
|
62
|
+
children = [ txt.slice(0, wix), m("span.errword", msg), txt.slice(wix + 6) ];
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
// No {word} macro: just return the message as-is
|
|
66
|
+
children = [ txt ];
|
|
67
|
+
}
|
|
68
|
+
return m(".error",
|
|
69
|
+
{
|
|
70
|
+
style: { visibility: "visible" },
|
|
71
|
+
onclick: (ev: Event) => { game.resetError(); ev.preventDefault(); }
|
|
72
|
+
},
|
|
73
|
+
[ glyph("exclamation-sign"), ...children ]
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
interface IAttributes {
|
|
81
|
+
view: IView;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export const BoardArea: ComponentFunc<IAttributes> = (initialVnode) => {
|
|
85
|
+
// Collection of components in the board (left-side) area
|
|
86
|
+
const view = initialVnode.attrs.view;
|
|
87
|
+
const model = view.model;
|
|
88
|
+
return {
|
|
89
|
+
view: () => {
|
|
90
|
+
const game = model.game;
|
|
91
|
+
let r: VnodeChildren = [];
|
|
92
|
+
if (game) {
|
|
93
|
+
r = [
|
|
94
|
+
m(Board, { view, review: false }),
|
|
95
|
+
m(Rack, { view, review: false }),
|
|
96
|
+
m(Buttons, { view }),
|
|
97
|
+
m(Errors, { game }),
|
|
98
|
+
m(GameOver, { game }),
|
|
99
|
+
];
|
|
100
|
+
r = r.concat(view.vwDialogs());
|
|
101
|
+
}
|
|
102
|
+
return m(".board-area", r);
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
interface ITileSquareAttributes {
|
|
108
|
+
view: IView;
|
|
109
|
+
coord: string;
|
|
110
|
+
opponent: boolean;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export const TileSquare: ComponentFunc<ITileSquareAttributes> = (initialVnode) => {
|
|
114
|
+
// Return a td element that wraps a tile on the board.
|
|
115
|
+
// If the opponent flag is true, we put an '.opp' class on the td
|
|
116
|
+
const view = initialVnode.attrs.view;
|
|
117
|
+
const model = view.model;
|
|
118
|
+
return {
|
|
119
|
+
view: (vnode) => {
|
|
120
|
+
const coord = vnode.attrs.coord;
|
|
121
|
+
const game = model.game;
|
|
122
|
+
if (!game) return undefined;
|
|
123
|
+
// The square contains a tile, so we don't allow dropping a tile on it
|
|
124
|
+
// and don't wish to allow a parent element to accept a drop either.
|
|
125
|
+
// Indicate this by including the not-target class.
|
|
126
|
+
return m("td.not-target",
|
|
127
|
+
{
|
|
128
|
+
id: "sq_" + coord,
|
|
129
|
+
class: game.squareClass(coord),
|
|
130
|
+
},
|
|
131
|
+
m(Tile, { view, coord: coord, opponent: false })
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
export const ReviewTileSquare: ComponentFunc<ITileSquareAttributes> = (initialVnode) => {
|
|
138
|
+
// Return a td element that wraps an 'inert' tile in a review screen.
|
|
139
|
+
// If the opponent flag is true, we put an '.opp' class on the td
|
|
140
|
+
const view = initialVnode.attrs.view;
|
|
141
|
+
const model = view.model;
|
|
142
|
+
return {
|
|
143
|
+
view: (vnode) => {
|
|
144
|
+
const game = model.game;
|
|
145
|
+
if (!game) return undefined;
|
|
146
|
+
const coord = vnode.attrs.coord;
|
|
147
|
+
let cls = game.squareClass(coord) || "";
|
|
148
|
+
if (cls)
|
|
149
|
+
cls = "." + cls;
|
|
150
|
+
if (vnode.attrs.opponent)
|
|
151
|
+
cls += ".opp";
|
|
152
|
+
return m("td" + cls, { id: "sq_" + coord }, vnode.children);
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
interface IDropTargetSquareAttributes {
|
|
158
|
+
view: IView;
|
|
159
|
+
coord: string;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export const DropTargetSquare: ComponentFunc<IDropTargetSquareAttributes> = (initialVnode) => {
|
|
163
|
+
// Return a td element that is a target for dropping tiles
|
|
164
|
+
const view = initialVnode.attrs.view;
|
|
165
|
+
const model = view.model;
|
|
166
|
+
return {
|
|
167
|
+
view: (vnode) => {
|
|
168
|
+
const game = model.game;
|
|
169
|
+
if (!game) return undefined;
|
|
170
|
+
const coord = vnode.attrs.coord;
|
|
171
|
+
let cls = game.squareClass(coord) || "";
|
|
172
|
+
if (cls)
|
|
173
|
+
cls = "." + cls;
|
|
174
|
+
// Mark the cell with the 'blinking' class if it is the drop
|
|
175
|
+
// target of a pending blank tile dialog
|
|
176
|
+
if (game.askingForBlank !== null && game.askingForBlank.to == coord)
|
|
177
|
+
cls += ".blinking";
|
|
178
|
+
if (coord == game.startSquare && game.localturn)
|
|
179
|
+
// Unoccupied start square, first move
|
|
180
|
+
cls += ".center";
|
|
181
|
+
return m("td.drop-target" + cls,
|
|
182
|
+
{
|
|
183
|
+
id: "sq_" + coord,
|
|
184
|
+
onclick: (ev: Event) => {
|
|
185
|
+
// If a square is selected (blinking red) and
|
|
186
|
+
// we click on an empty square, move the selected tile
|
|
187
|
+
// to the clicked square
|
|
188
|
+
if (game.selectedSq !== null) {
|
|
189
|
+
ev.stopPropagation();
|
|
190
|
+
game.attemptMove(game.selectedSq, coord);
|
|
191
|
+
game.selectedSq = null;
|
|
192
|
+
(ev.currentTarget as HTMLElement).classList.remove("sel");
|
|
193
|
+
view.updateScale();
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
onmouseover: (ev: Event) => {
|
|
198
|
+
// If a tile is selected, show a red selection square
|
|
199
|
+
// around this square when the mouse is over it
|
|
200
|
+
if (game.selectedSq !== null)
|
|
201
|
+
(ev.currentTarget as HTMLElement).classList.add("sel");
|
|
202
|
+
},
|
|
203
|
+
onmouseout: (ev: Event) => {
|
|
204
|
+
(ev.currentTarget as HTMLElement).classList.remove("sel");
|
|
205
|
+
}
|
|
206
|
+
},
|
|
207
|
+
vnode.children
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
interface IBoardAttributes {
|
|
214
|
+
view: IView;
|
|
215
|
+
review: boolean;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export const Board: ComponentFunc<IBoardAttributes> = (initialVnode) => {
|
|
219
|
+
// The game board, a 15x15 table plus row (A-O) and column (1-15) identifiers
|
|
220
|
+
|
|
221
|
+
const { view, review } = initialVnode.attrs;
|
|
222
|
+
const model = view.model;
|
|
223
|
+
|
|
224
|
+
function colid(): VnodeChildren {
|
|
225
|
+
// The column identifier row
|
|
226
|
+
let r: VnodeChildren = [];
|
|
227
|
+
r.push(m("td"));
|
|
228
|
+
for (let col = 1; col <= 15; col++)
|
|
229
|
+
r.push(m("td", col.toString()));
|
|
230
|
+
return m("tr.colid", r);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function row(rowid: string): VnodeChildren {
|
|
234
|
+
// Each row of the board
|
|
235
|
+
let r: VnodeChildren = [];
|
|
236
|
+
const game = model.game;
|
|
237
|
+
r.push(m("td.rowid", { key: "R" + rowid }, rowid));
|
|
238
|
+
for (let col = 1; col <= 15; col++) {
|
|
239
|
+
const coord = rowid + col.toString();
|
|
240
|
+
if (game && (coord in game.tiles))
|
|
241
|
+
// There is a tile in this square: render it
|
|
242
|
+
r.push(m(TileSquare, { view, key: coord, coord: coord, opponent: false }));
|
|
243
|
+
else if (review)
|
|
244
|
+
// Empty, inert square
|
|
245
|
+
r.push(m(ReviewTileSquare, { view, key: coord, coord: coord, opponent: false }));
|
|
246
|
+
else
|
|
247
|
+
// Empty square which is a drop target
|
|
248
|
+
r.push(m(DropTargetSquare, { view, key: coord, coord: coord }));
|
|
249
|
+
}
|
|
250
|
+
return m("tr", r);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function allrows(): VnodeChildren {
|
|
254
|
+
// Return a list of all rows on the board
|
|
255
|
+
let r: VnodeChildren = [];
|
|
256
|
+
r.push(colid());
|
|
257
|
+
const rows = "ABCDEFGHIJKLMNO";
|
|
258
|
+
for (const rw of rows)
|
|
259
|
+
r.push(row(rw));
|
|
260
|
+
return r;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function zoomIn() {
|
|
264
|
+
view.boardScale = 1.5;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function zoomOut() {
|
|
268
|
+
if (view.boardScale != 1.0) {
|
|
269
|
+
view.boardScale = 1.0;
|
|
270
|
+
setTimeout(view.resetScale);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return {
|
|
275
|
+
view: (vnode) => {
|
|
276
|
+
const scale = view.boardScale || 1.0;
|
|
277
|
+
let attrs: VnodeAttrs = {};
|
|
278
|
+
// Add handlers for pinch zoom functionality
|
|
279
|
+
addPinchZoom(attrs, zoomIn, zoomOut);
|
|
280
|
+
if (scale != 1.0)
|
|
281
|
+
attrs.style = `transform: scale(${scale})`;
|
|
282
|
+
return m(".board",
|
|
283
|
+
{ id: "board-parent" },
|
|
284
|
+
m("table.board", attrs, m("tbody", allrows()))
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export const Rack: ComponentFunc<IBoardAttributes> = (initialVnode) => {
|
|
291
|
+
// A rack of 7 tiles
|
|
292
|
+
const { view, review } = initialVnode.attrs;
|
|
293
|
+
const model = view.model;
|
|
294
|
+
return {
|
|
295
|
+
view: () => {
|
|
296
|
+
const game = model.game;
|
|
297
|
+
if (!game) return undefined;
|
|
298
|
+
let r: VnodeChildren = [];
|
|
299
|
+
// If review==true, this is a review rack
|
|
300
|
+
// that is not a drop target and whose color reflects the
|
|
301
|
+
// currently shown move.
|
|
302
|
+
// If opponent==true, we're showing the opponent's rack
|
|
303
|
+
const reviewMove = model.reviewMove ?? 0;
|
|
304
|
+
const opponent = review && (reviewMove > 0) && (reviewMove % 2 === game.player);
|
|
305
|
+
for (let i = 1; i <= RACK_SIZE; i++) {
|
|
306
|
+
const coord = 'R' + i.toString();
|
|
307
|
+
if (game && (coord in game.tiles)) {
|
|
308
|
+
// We have a tile in this rack slot, but it is a drop target anyway
|
|
309
|
+
if (review) {
|
|
310
|
+
r.push(
|
|
311
|
+
m(ReviewTileSquare, { view, coord: coord, opponent: opponent },
|
|
312
|
+
m(Tile, { view, coord: coord, opponent: opponent })
|
|
313
|
+
)
|
|
314
|
+
);
|
|
315
|
+
} else {
|
|
316
|
+
r.push(
|
|
317
|
+
m(DropTargetSquare, { view, coord: coord },
|
|
318
|
+
m(Tile, { view, coord: coord, opponent: false })
|
|
319
|
+
)
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
else if (review) {
|
|
324
|
+
r.push(m(ReviewTileSquare, { view, coord: coord, opponent: false }));
|
|
325
|
+
}
|
|
326
|
+
else {
|
|
327
|
+
r.push(m(DropTargetSquare, { view, coord: coord }));
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
return m(".rack-row", [
|
|
331
|
+
m(".rack-left", m(ButtonsLeftOfRack, { view })),
|
|
332
|
+
m(".rack", m("table.board", m("tbody", m("tr", r))))
|
|
333
|
+
]);
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
};
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
/*
|
|
2
|
+
|
|
3
|
+
Buttons.ts
|
|
4
|
+
|
|
5
|
+
Buttons below rack component
|
|
6
|
+
|
|
7
|
+
Copyright (C) 2024 Miðeind ehf.
|
|
8
|
+
Author: Vilhjalmur Thorsteinsson
|
|
9
|
+
|
|
10
|
+
The Creative Commons Attribution-NonCommercial 4.0
|
|
11
|
+
International Public License (CC-BY-NC 4.0) applies to this software.
|
|
12
|
+
For further information, see https://github.com/mideind/Netskrafl
|
|
13
|
+
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { ts } from "./i18n";
|
|
17
|
+
import { AnimatedExploLogo } from "./logo";
|
|
18
|
+
import { Component, ComponentFunc, m, VnodeAttrs, VnodeChildren } from "./mithril";
|
|
19
|
+
import { IGame, IView } from "./types";
|
|
20
|
+
import { buttonOut, buttonOver, glyph, nbsp } from "./util";
|
|
21
|
+
|
|
22
|
+
export function makeButton(
|
|
23
|
+
cls: string, disabled: boolean, onclick: () => void,
|
|
24
|
+
title?: string, children?: VnodeChildren, id?: string
|
|
25
|
+
): VnodeChildren {
|
|
26
|
+
// Create a button element, wrapping the disabling logic
|
|
27
|
+
// and other boilerplate
|
|
28
|
+
const attr: VnodeAttrs = {
|
|
29
|
+
onmouseout: buttonOut,
|
|
30
|
+
onmouseover: buttonOver,
|
|
31
|
+
};
|
|
32
|
+
if (title)
|
|
33
|
+
attr.title = title;
|
|
34
|
+
if (id)
|
|
35
|
+
attr.id = id;
|
|
36
|
+
if (disabled)
|
|
37
|
+
attr.onclick = (ev: Event) => ev.preventDefault();
|
|
38
|
+
else
|
|
39
|
+
attr.onclick = (ev: Event) => {
|
|
40
|
+
onclick && onclick();
|
|
41
|
+
ev.preventDefault();
|
|
42
|
+
};
|
|
43
|
+
return m(
|
|
44
|
+
"." + cls + (disabled ? ".disabled" : ""),
|
|
45
|
+
attr, children // children may be omitted
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const Score: Component<{ game: IGame }> = {
|
|
50
|
+
view: (vnode) => {
|
|
51
|
+
// Shows the score of the current word
|
|
52
|
+
const { game } = vnode.attrs;
|
|
53
|
+
let sc = [".score"];
|
|
54
|
+
if (game.manual)
|
|
55
|
+
sc.push("manual");
|
|
56
|
+
else
|
|
57
|
+
if (game.wordGood) {
|
|
58
|
+
sc.push("word-good");
|
|
59
|
+
if (game.currentScore !== undefined && game.currentScore >= 50)
|
|
60
|
+
sc.push("word-great");
|
|
61
|
+
}
|
|
62
|
+
let txt = (game.currentScore === undefined ? "?" : game.currentScore.toString())
|
|
63
|
+
return m(sc.join("."), { title: txt }, txt);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const RecallButton: Component<{ view: IView; disabled?: boolean; }> = {
|
|
68
|
+
view: (vnode) => {
|
|
69
|
+
// Create a tile recall button
|
|
70
|
+
const { view, disabled } = vnode.attrs;
|
|
71
|
+
const game = view.model.game;
|
|
72
|
+
if (!game) return undefined;
|
|
73
|
+
return makeButton(
|
|
74
|
+
"recallbtn", !!disabled,
|
|
75
|
+
() => { game.resetRack(); view.updateScale(); },
|
|
76
|
+
ts("Færa stafi aftur í rekka"), glyph("down-arrow")
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const ScrambleButton: Component<{ view: IView; disabled?: boolean; }> = {
|
|
82
|
+
view: (vnode) => {
|
|
83
|
+
// Create a tile scramble button
|
|
84
|
+
const { view, disabled } = vnode.attrs;
|
|
85
|
+
const game = view.model.game;
|
|
86
|
+
if (!game) return undefined;
|
|
87
|
+
return makeButton(
|
|
88
|
+
"scramblebtn", !!disabled,
|
|
89
|
+
() => game.rescrambleRack(), // Note: plain game.rescrambleRack doesn't work here
|
|
90
|
+
ts("Stokka upp rekka"), glyph("random")
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
export const ButtonsLeftOfRack: Component<{ view: IView }> = {
|
|
96
|
+
view: (vnode) => {
|
|
97
|
+
// The button to the left of the rack in the mobile UI
|
|
98
|
+
const { view } = vnode.attrs;
|
|
99
|
+
const game = view.model.game;
|
|
100
|
+
if (!game) return undefined;
|
|
101
|
+
const s = game.buttonState();
|
|
102
|
+
if (s.showRecall && !s.showingDialog)
|
|
103
|
+
// Show a 'Recall tiles' button
|
|
104
|
+
return m(RecallButton, { view });
|
|
105
|
+
if (s.showScramble && !s.showingDialog)
|
|
106
|
+
return m(ScrambleButton, { view });
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
interface IAttributes {
|
|
112
|
+
view: IView;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export const Buttons: ComponentFunc<IAttributes> = (initialVnode) => {
|
|
116
|
+
|
|
117
|
+
const view = initialVnode.attrs.view;
|
|
118
|
+
const model = view.model;
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
view: () => {
|
|
122
|
+
// The set of buttons below the game board, alongside the rack (fullscreen view)
|
|
123
|
+
// or below the rack (mobile view)
|
|
124
|
+
const game = model.game;
|
|
125
|
+
let r: VnodeChildren = [];
|
|
126
|
+
if (!game) return undefined;
|
|
127
|
+
const s = game.buttonState();
|
|
128
|
+
r.push(m(".word-check" +
|
|
129
|
+
(s.wordGood ? ".word-good" : "") +
|
|
130
|
+
(s.wordBad ? ".word-bad" : "")));
|
|
131
|
+
if (s.showChallenge) {
|
|
132
|
+
// Show a button that allows the player to challenge the opponent's
|
|
133
|
+
// last move
|
|
134
|
+
const disabled = (s.tilesPlaced || s.showingDialog) && !s.lastChallenge;
|
|
135
|
+
r.push(
|
|
136
|
+
makeButton(
|
|
137
|
+
"challenge", disabled,
|
|
138
|
+
() => game.submitChallenge(), // Note: plain game.submitChallenge doesn't work here
|
|
139
|
+
'Véfenging (röng kostar 10 stig)', glyph("ban-circle")
|
|
140
|
+
)
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
if (s.showRecall) {
|
|
144
|
+
// Show button to recall tiles from the board into the rack
|
|
145
|
+
r.push(m(RecallButton, { view, disabled: s.showingDialog }));
|
|
146
|
+
}
|
|
147
|
+
if (s.showScramble) {
|
|
148
|
+
// Show button to scramble (randomly reorder) the rack tiles
|
|
149
|
+
r.push(m(ScrambleButton, { view, disabled: s.showingDialog }));
|
|
150
|
+
}
|
|
151
|
+
if (s.showMove) {
|
|
152
|
+
// "Plain" move button for fullscreen
|
|
153
|
+
const submit_move = ts("submit_move"); // 'Move' or 'Leika'
|
|
154
|
+
r.push(
|
|
155
|
+
makeButton(
|
|
156
|
+
"submitmove", !s.tilesPlaced || s.showingDialog,
|
|
157
|
+
() => { game.submitMove(); view.updateScale(); },
|
|
158
|
+
submit_move, [submit_move, glyph("play")]
|
|
159
|
+
)
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
if (s.showMoveMobile) {
|
|
163
|
+
// Submit-Move button on mobile, which also shows the score
|
|
164
|
+
// and whether the move is good or bad
|
|
165
|
+
let classes: string[] = ["submitmove"];
|
|
166
|
+
let wordIsPlayable = game.currentScore !== undefined;
|
|
167
|
+
if (game.manual) {
|
|
168
|
+
classes.push("manual")
|
|
169
|
+
} else if (s.wordGood) {
|
|
170
|
+
classes.push("word-good");
|
|
171
|
+
if (game.currentScore !== undefined && game.currentScore >= 50)
|
|
172
|
+
classes.push("word-great");
|
|
173
|
+
} else if (s.wordBad) {
|
|
174
|
+
classes.push("word-bad");
|
|
175
|
+
wordIsPlayable = false;
|
|
176
|
+
}
|
|
177
|
+
const text = (game.currentScore === undefined) ? "?" : game.currentScore.toString();
|
|
178
|
+
let legend: VnodeChildren[] = [m("span.score-mobile", text)];
|
|
179
|
+
if (s.canPlay && wordIsPlayable)
|
|
180
|
+
legend.push(glyph("play"));
|
|
181
|
+
else
|
|
182
|
+
legend.push(glyph("remove"));
|
|
183
|
+
let action: () => void;
|
|
184
|
+
if (s.canPlay) {
|
|
185
|
+
if (wordIsPlayable)
|
|
186
|
+
action = () => { game.submitMove(); view.updateScale(); };
|
|
187
|
+
else
|
|
188
|
+
action = () => { /* TODO: Add some kind of feedback? */ };
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
action = () => {
|
|
192
|
+
// Make the 'opp-turn' flash, to remind the user that it's not her turn
|
|
193
|
+
const el = document.querySelector("div.opp-turn") as HTMLElement;
|
|
194
|
+
if (el) {
|
|
195
|
+
el.classList.toggle("flashing", true);
|
|
196
|
+
setTimeout(() => el.classList.toggle("flashing", false), 1200);
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
r.push(
|
|
201
|
+
makeButton(
|
|
202
|
+
classes.join("."), s.showingDialog, action, text, legend, "move-mobile"
|
|
203
|
+
)
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
if (s.showForceResignMobile) {
|
|
207
|
+
// Force resignation button (only shown on mobile,
|
|
208
|
+
// and only if submit move button is not shown)
|
|
209
|
+
const txt = ts("Þvinga til uppgjafar");
|
|
210
|
+
r.push(
|
|
211
|
+
makeButton(
|
|
212
|
+
"force-resign",
|
|
213
|
+
s.showingDialog,
|
|
214
|
+
() => game.forceResign(), // Note: plain game.forceResign doesn't work here
|
|
215
|
+
txt,
|
|
216
|
+
txt
|
|
217
|
+
)
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
if (s.showPass) {
|
|
221
|
+
// Pass move: shown if no tiles have been placed
|
|
222
|
+
// and we're not showing a dialog, or if this is
|
|
223
|
+
// the last move by the opponent in a manual game
|
|
224
|
+
r.push(
|
|
225
|
+
makeButton(
|
|
226
|
+
"submitpass",
|
|
227
|
+
(s.tilesPlaced || s.showingDialog) && !s.lastChallenge,
|
|
228
|
+
() => game.submitPass(), // Note: plain game.submitPass doesn't work here
|
|
229
|
+
ts("Pass"), glyph("forward")
|
|
230
|
+
)
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
if (s.showExchange) {
|
|
234
|
+
// Exchange tiles from the rack
|
|
235
|
+
r.push(
|
|
236
|
+
makeButton(
|
|
237
|
+
"submitexchange",
|
|
238
|
+
s.tilesPlaced || s.showingDialog || !s.exchangeAllowed,
|
|
239
|
+
() => game.submitExchange(), // Note: plain game.submitExchange doesn't work here
|
|
240
|
+
ts("Skipta stöfum"), glyph("refresh")
|
|
241
|
+
)
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
if (s.showResign) {
|
|
245
|
+
// Resign the game
|
|
246
|
+
r.push(
|
|
247
|
+
makeButton(
|
|
248
|
+
"submitresign", s.showingDialog,
|
|
249
|
+
() => game.submitResign(), // Note: plain game.submitResign doesn't work here
|
|
250
|
+
ts("Gefa viðureign"), glyph("fire")
|
|
251
|
+
)
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
if (!s.gameOver && !s.localTurn && !game.moveInProgress && game.player !== null) {
|
|
255
|
+
// Indicate that it is the opponent's turn; offer to force a resignation
|
|
256
|
+
// if the opponent hasn't moved for 14 days
|
|
257
|
+
r.push(
|
|
258
|
+
m(".opp-turn",
|
|
259
|
+
{ style: { visibility: "visible" } },
|
|
260
|
+
[
|
|
261
|
+
m("span.move-indicator"),
|
|
262
|
+
nbsp(),
|
|
263
|
+
m("strong", game.nickname[1 - game.player]),
|
|
264
|
+
ts(" á leik"),
|
|
265
|
+
nbsp(),
|
|
266
|
+
// The following inline button is only
|
|
267
|
+
// displayed in the fullscreen UI
|
|
268
|
+
s.tardyOpponent ? m("span.yesnobutton",
|
|
269
|
+
{
|
|
270
|
+
id: 'force-resign',
|
|
271
|
+
onclick: (ev: Event) => {
|
|
272
|
+
ev.preventDefault();
|
|
273
|
+
game.forceResign();
|
|
274
|
+
},
|
|
275
|
+
onmouseout: buttonOut,
|
|
276
|
+
onmouseover: buttonOver,
|
|
277
|
+
title: ts("14 dagar liðnir án leiks")
|
|
278
|
+
},
|
|
279
|
+
ts("Þvinga til uppgjafar")
|
|
280
|
+
) : ""
|
|
281
|
+
]
|
|
282
|
+
)
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
if (s.tilesPlaced) {
|
|
286
|
+
// Show the score of the current move (not visible on mobile)
|
|
287
|
+
const sc = m(Score, { game });
|
|
288
|
+
sc && r.push(sc);
|
|
289
|
+
}
|
|
290
|
+
// Is the server processing a move?
|
|
291
|
+
if (game?.moveInProgress) {
|
|
292
|
+
r.push(
|
|
293
|
+
m(".waitmove",
|
|
294
|
+
m(".animated-waitmove",
|
|
295
|
+
m(AnimatedExploLogo, { msStepTime: 100, width: 38, withCircle: false })
|
|
296
|
+
)
|
|
297
|
+
)
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
return m(".buttons", r);
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
};
|