3 // Copyright 2020 Ian Jackson
4 // SPDX-License-Identifier: AGPL-3.0-or-later
5 // There is NO WARRANTY. -->
13 // <use id="use{}", href="#piece{}" x= y= >
14 // .piece piece id (static)
15 // container to allow quick movement and hang stuff off
25 // .dragraise dragged more than this ? raise to top!
27 // currently-displayed version of the piece
28 // to allow addition/removal of selected indication
29 // contains 1 or 3 subelements:
30 // one is straight from server and not modified
31 // one is possible <use href="#select{}" >
32 // one is possible <use href="#halo{}" >
35 // generated by server, referenced by JS in pelem for selection
38 // generated by server, reserved for Piece trait impl
40 type PieceId = string;
41 type PlayerId = string;
42 type Pos = [number, number];
43 type ClientSeq = number;
44 type Generation = number;
45 type UoKind = 'Client' | "Global"| "Piece" | "ClientExtra" | "GlobalExtra";
46 type WhatResponseToClientOp = "Predictable" | "Unpredictable" | "UpdateSvg";
47 type Timestamp = number; // unix time_t, will break in 285My
48 type Layout = 'Portrait' | 'Landscape';
50 type UoDescription = {
52 wrc: WhatResponseToClientOp,
58 type UoRecord = UoDescription & {
59 targets: PieceId[] | null,
65 held : PlayerId | null,
67 cseq_updatesvg : number | null,
72 uos : UoDescription[],
73 uelem : SVGGraphicsElement,
74 delem : SVGGraphicsElement,
75 pelem : SVGGraphicsElement,
76 queued_moves : number,
77 last_seen_moved : DOMHighResTimeStamp | null, // non-0 means halo'd
80 let wasm : wasm_bindgen.InitOutput;
82 let pieces : { [piece: string]: PieceInfo } = Object.create(null);
84 type MessageHandler = (op: Object) => void;
85 type PieceHandler = (piece: PieceId, p: PieceInfo, info: Object) => void;
86 type PieceErrorHandler = (piece: PieceId, p: PieceInfo, m: PieceOpError)
88 interface DispatchTable<H> { [key: string]: H };
90 // xxx turn all var into let
91 // xxx any exceptions should have otter in them or something
92 var globalinfo_elem : HTMLElement;
94 var general_timeout : number = 10000;
95 var messages : DispatchTable<MessageHandler> = Object();
96 var pieceops : DispatchTable<PieceHandler> = Object();
97 var update_error_handlers : DispatchTable<MessageHandler> = Object();
98 var piece_error_handlers : DispatchTable<PieceErrorHandler> = Object();
99 var our_dnd_type = "text/puvnex-game-server-dummy";
100 var api_queue : [string, Object][] = [];
101 var api_posting = false;
103 var gen : Generation = 0;
104 var cseq : ClientSeq = 0;
106 var uo_map : { [k: string]: UoRecord | null } = Object.create(null);
107 var keyops_local : { [opname: string]: (uo: UoRecord) => void } = Object();
108 var last_log_ts: wasm_bindgen.TimestampAbbreviator;
109 var last_zoom_factor : number = 1.0;
110 var firefox_bug_zoom_factor_compensation : number = 1.0;
111 var gen_update_hook : () => void = function() { }
114 var space : SVGGraphicsElement;
115 var pieces_marker : SVGGraphicsElement;
116 var defs_marker : SVGGraphicsElement;
117 var log_elem : HTMLElement;
118 var logscroll_elem : HTMLElement;
119 var status_node : HTMLElement;
120 var uos_node : HTMLElement;
121 var zoom_val : HTMLInputElement;
122 var zoom_btn : HTMLInputElement;
123 var links_elem : HTMLElement;
124 var wresting: boolean;
126 const uo_kind_prec : { [kind: string]: number } = {
137 var players : { [player: string]: PlayerInfo };
139 type MovementRecord = {
142 this_motion: DOMHighResTimeStamp,
144 var movements : MovementRecord[] = [];
146 function xhr_post_then(url : string, data: string,
147 good : (xhr: XMLHttpRequest) => void) {
148 var xhr : XMLHttpRequest = new XMLHttpRequest();
149 xhr.onreadystatechange = function(){
150 if (xhr.readyState != XMLHttpRequest.DONE) { return; }
151 if (xhr.status != 200) { xhr_report_error(xhr); }
154 xhr.timeout = general_timeout;
155 xhr.open('POST',url);
156 xhr.setRequestHeader('Content-Type','application/json');
160 function xhr_report_error(xhr: XMLHttpRequest) {
162 statusText : xhr.statusText,
163 responseText : xhr.responseText,
167 function json_report_error(error_for_json: Object) {
168 let error_message = JSON.stringify(error_for_json);
169 string_report_error(error_message);
172 function string_report_error(error_message: String) {
173 let errornode = document.getElementById('error')!;
174 errornode.textContent += '\nError (reloading may help?):' + error_message;
175 console.error("ERROR reported via log", error_message);
176 // todo want to fix this for at least basic game reconfigs, auto-reload?
179 function api(meth: string, data: Object) {
180 api_queue.push([meth, data]);
183 function api_delay(meth: string, data: Object) {
184 if (api_queue.length==0) window.setTimeout(api_check, 10);
185 api_queue.push([meth, data]);
187 function api_check() {
188 if (api_posting) { return; }
189 if (!api_queue.length) { return; }
191 var [meth, data] = api_queue.shift()!;
192 if (meth != 'm') break;
193 let piece = (data as any).piece;
194 let p = pieces[piece];
195 if (p == null) break;
197 if (p.queued_moves == 0) break;
200 xhr_post_then('/_/api/'+meth, JSON.stringify(data), api_posted);
202 function api_posted() {
207 function api_piece(f: (meth: string, payload: Object) => void,
209 piece: PieceId, p: PieceInfo,
223 function svg_element(id: string): SVGGraphicsElement | null {
224 let elem = document.getElementById(id);
225 return elem as unknown as (SVGGraphicsElement | null);
227 function piece_element(base: string, piece: PieceId): SVGGraphicsElement | null
229 return svg_element(base+piece);
232 // ----- key handling -----
234 function recompute_keybindings() {
235 uo_map = Object.create(null);
236 let all_targets = [];
237 for (let piece of Object.keys(pieces)) {
238 let p = pieces[piece];
239 if (p.held != us) continue;
240 all_targets.push(piece);
241 for (var uo of p.uos) {
242 let currently = uo_map[uo.def_key];
243 if (currently === null) continue;
244 if (currently !== undefined) {
245 if (currently.opname != uo.opname) {
246 uo_map[uo.def_key] = null;
254 uo_map[uo.def_key] = currently;
256 currently.desc = currently.desc < uo.desc ? currently.desc : uo.desc;
257 currently.targets!.push(piece);
260 let add_uo = function(targets: PieceId[] | null, uo: UoDescription) {
261 uo_map[uo.def_key] = {
266 if (all_targets.length) {
267 add_uo(all_targets, {
274 add_uo(all_targets, {
279 desc: "rotate right",
281 add_uo(all_targets, {
286 desc: "send to bottom (below other pieces)",
289 if (all_targets.length) {
291 for (let t of all_targets) {
292 got |= 1 << Number(pieces[t]!.pinned);
295 add_uo(all_targets, {
299 desc: 'Pin to table',
302 } else if (got == 2) {
303 add_uo(all_targets, {
307 desc: 'Unpin from table',
316 desc: wresting ? 'Exit wresting mode' : 'Enter wresting mode',
319 var uo_keys = Object.keys(uo_map);
320 uo_keys.sort(function (ak,bk) {
323 return uo_kind_prec[a.kind] - uo_kind_prec[b.kind]
324 || ak.localeCompare(bk);
327 for (let celem = uos_node.firstElementChild;
330 var nextelem = celem.nextElementSibling;
331 let cid = celem.getAttribute("id");
332 if (cid == "uos-mid") mid_elem = celem;
333 else if (celem.getAttribute("class") == 'uos-mid') { }
336 for (var kk of uo_keys) {
337 let uo = uo_map[kk]!;
338 let prec = uo_kind_prec[uo.kind];
339 let ent = document.createElement('div');
340 ent.innerHTML = '<b>' + kk + '</b> ' + uo.desc;
342 ent.setAttribute('class','uokey-l');
343 uos_node.insertBefore(ent, mid_elem);
345 ent.setAttribute('class','uokey-r');
346 uos_node.appendChild(ent);
351 function some_keydown(e: KeyboardEvent) {
352 // https://developer.mozilla.org/en-US/docs/Web/API/Document/keydown_event
353 // says to do this, something to do with CJK composition.
354 // https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent
355 // says that keyCode is deprecated
356 // my tsc says this isComposing thing doesn't exist. wat.
357 if ((e as any).isComposing /* || e.keyCode === 229 */) return;
359 let uo = uo_map[e.key];
360 if (uo === undefined || uo === null) return;
362 console.log('KEY UO', e, uo);
363 if (uo.kind == 'Client' || uo.kind == 'ClientExtra') {
364 let f = keyops_local[uo.opname];
368 if (!(uo.kind == 'Global' || uo.kind == 'GlobalExtra'))
369 throw 'bad kind '+uo.kind;
371 if (uo.wrc == 'UpdateSvg' || uo.wrc == 'Predictable') {
372 for (var piece of uo.targets!) {
373 let p = pieces[piece]!;
374 api_piece(api, 'k', piece, p, { opname: uo.opname, wrc: uo.wrc });
375 if (uo.wrc == 'UpdateSvg') {
376 p.cseq_updatesvg = p.cseq;
377 redisplay_ancillaries(piece,p);
383 keyops_local['left' ] = function (uo: UoRecord) { rotate_targets(uo, +1); }
384 keyops_local['right'] = function (uo: UoRecord) { rotate_targets(uo, -1); }
386 function rotate_targets(uo: UoRecord, dangle: number): boolean {
387 for (let piece of uo.targets!) {
388 let p = pieces[piece]!;
389 p.angle += dangle + 8;
392 api_piece(api, 'rotate', piece,p, p.angle);
394 recompute_keybindings();
398 type LowerTodoItem = {
404 type LowerTodoList = { [piece: string]: LowerTodoItem };
406 keyops_local['lower'] = function (uo: UoRecord) { lower_targets(uo); }
408 function lower_targets(uo: UoRecord): boolean {
409 function target_treat_pinned(p: PieceInfo): boolean {
410 return wresting || p.pinned;;
413 let targets_todo : LowerTodoList = Object.create(null);
415 for (let piece of uo.targets!) {
416 let p = pieces[piece]!;
417 let pinned = target_treat_pinned(p);
418 targets_todo[piece] = { p, piece, pinned, };
420 let problem = lower_pieces(targets_todo);
421 if (problem !== null) {
422 add_log_message('Cannot lower: ' + problem);
428 function lower_pieces(targets_todo: LowerTodoList):
431 // This is a bit subtle. We don't want to lower below pinned pieces
432 // (unless we are pinned too, or the user is wresting). But maybe
433 // the pinned pieces aren't already at the bottom. For now we will
434 // declare that all pinned pieces "should" be below all non-pinned
435 // ones. Not as an invariant, but as a thing we will do here to try
436 // to make a sensible result. We implement this as follows: if we
437 // find pinned pieces above non-pinned pieces, we move those pinned
438 // pieces to the bottom too, just below us, preserving their
441 // Disregarding pinned targets:
443 // Z <some stuff not including any unpinned targets>
445 // topmost unpinned target *
447 // B unpinned non-target
448 // B | unpinned target *
449 // B | pinned non-target, mis-stacked *
452 // bottommost unpinned non-target
453 // if that is below topmost unpinned target
454 // <- tomove_unpinned: insert targets from * here Q ->
455 // <- tomove_misstacked: insert non-targets from * here Q->
457 // A pinned things (nomove_pinned)
458 // <- tomove_pinned: insert all pinned targets here P ->
460 // When wresting, treat all targets as pinned.
466 // bottom of the stack order first
467 let tomove_unpinned : Entry[] = [];
468 let tomove_misstacked : Entry[] = [];
469 let nomove_pinned : Entry[] = [];
470 let tomove_pinned : Entry[] = [];
471 let bottommost_unpinned : Entry | null = null;
473 let n_targets_todo_unpinned = 0;
474 for (const piece of Object.keys(targets_todo)) {
475 let p = targets_todo[piece];
476 if (!p.pinned) n_targets_todo_unpinned++;
479 let walk = pieces_marker;
480 for (;;) { // starting at the bottom of the stack order
481 if (n_targets_todo_unpinned == 0
482 && bottommost_unpinned !== null) {
483 // no unpinned targets left, state Z, we can stop now
484 console.log('LOWER STATE Z FINISHED');
487 if (Object.keys(targets_todo).length == 0 &&
488 bottommost_unpinned !== null) {
489 console.log('LOWER NO TARGETS BUT UNPINNED!', n_targets_todo_unpinned);
493 let new_walk = walk.nextElementSibling;
494 if (new_walk == null) {
495 console.log('LOWER WALK NO SIBLING!');
498 walk = new_walk as SVGGraphicsElement;
499 let piece = walk.dataset.piece;
501 console.log('LOWER WALK REACHED TOP');
505 let todo = targets_todo[piece];
507 console.log('LOWER WALK', piece, 'TODO', todo.pinned);
508 delete targets_todo[piece];
509 if (!todo.pinned) n_targets_todo_unpinned--;
510 (todo.pinned ? tomove_pinned : tomove_unpinned).push(todo);
514 let p = pieces[piece]!;
515 if (bottommost_unpinned === null) { // state A
517 console.log('LOWER WALK', piece, 'STATE A -> Z');
518 bottommost_unpinned = { p, piece };
520 console.log('LOWER WALK', piece, 'STATE A');
521 nomove_pinned.push({ p, piece });
528 console.log('LOWER WALK', piece, 'STATE B MIS-STACKED');
529 tomove_misstacked.push({ p, piece });
531 console.log('LOWER WALK', piece, 'STATE B');
536 bottommost_unpinned ? bottommost_unpinned.p.z :
537 walk.dataset.piece != null ? pieces[walk.dataset.piece!].z :
538 // rather a lack of things we are not adjusting!
539 wasm_bindgen.def_zcoord();
542 content: Entry[], // bottom to top
543 z_top: ZCoord | null,
544 z_bot: ZCoord | null,
547 let plan : PlanEntry[] = [];
549 let partQ = tomove_unpinned.concat(tomove_misstacked);
550 let partP = tomove_pinned;
552 if (nomove_pinned.length == 0) {
554 content: partQ.concat(partP),
562 z_bot: nomove_pinned[nomove_pinned.length-1].p.z,
565 z_top: nomove_pinned[0].p.z,
570 console.log('LOWER PLAN', plan);
572 for (const pe of plan) {
573 for (const e of pe.content) {
574 if (e.p.held != null && e.p.held != us) {
575 return "lowering would disturb a piece held by another player";
581 for (const pe of plan) {
582 if (pe.z_top != null) z_top = pe.z_top;
583 let z_bot = pe.z_bot;
584 let zrange = wasm_bindgen.range(z_bot, z_top, pe.content.length);
585 console.log('LOQER PLAN PE',
586 pe, z_bot, z_top, pe.content.length, zrange.debug());
587 for (const e of pe.content) {
589 piece_set_zlevel(e.piece, p, (oldtop_piece) => {
590 let z = zrange.next();
592 api_piece(api, "setz", e.piece, e.p, { z });
599 keyops_local['wrest'] = function (uo: UoRecord) {
600 wresting = !wresting;
601 document.getElementById('wresting-warning')!.innerHTML = !wresting ? "" :
602 " <strong>(wresting mode!)</strong>";
604 recompute_keybindings();
607 keyops_local['pin' ] = function (uo) {
608 if (!lower_targets(uo)) return;
611 keyops_local['unpin'] = function (uo) {
612 pin_unpin(uo, false);
615 function pin_unpin(uo: UoRecord, newpin: boolean) {
616 for (let piece of uo.targets!) {
617 let p = pieces[piece]!;
619 api_piece(api, 'pin', piece,p, newpin);
620 redisplay_ancillaries(piece,p);
622 recompute_keybindings();
625 // ----- clicking/dragging pieces -----
633 enum DRAGGING { // bitmask
641 var drag_pieces : DragInfo[] = [];
642 var dragging = DRAGGING.NO;
643 var dcx : number | null;
644 var dcy : number | null;
646 const DRAGTHRESH = 5;
648 function drag_add_piece(piece: PieceId, p: PieceInfo) {
651 dox: parseFloat(p.uelem.getAttributeNS(null,"x")!),
652 doy: parseFloat(p.uelem.getAttributeNS(null,"y")!),
656 function some_mousedown(e : MouseEvent) {
657 console.log('mousedown', e, e.clientX, e.clientY, e.target);
659 if (e.button != 0) { return }
660 if (e.altKey) { return }
661 if (e.metaKey) { return }
665 drag_mousedown(e, e.shiftKey);
669 function drag_mousedown(e : MouseEvent, shifted: boolean) {
670 var target = e.target as SVGGraphicsElement; // we check this just now!
671 var piece = target.dataset.piece!;
672 if (!piece) { return; }
673 let p = pieces[piece]!;
680 dragging = DRAGGING.MAYBE_UNGRAB;
681 drag_add_piece(piece,p); // contrive to have this one first
682 for (let tpiece of Object.keys(pieces)) {
683 if (tpiece == piece) continue;
684 let tp = pieces[tpiece]!;
685 if (tp.held != us) continue;
686 drag_add_piece(tpiece,tp);
688 } else if (held == null || wresting) {
689 if (p.pinned && !wresting) {
690 add_log_message('That piece is pinned to the table.');
696 dragging = DRAGGING.MAYBE_GRAB;
697 drag_add_piece(piece,p);
698 set_grab(piece,p, us);
699 api_piece(api, wresting ? 'wrest' : 'grab', piece,p, { });
701 add_log_message('That piece is held by another player.');
707 window.addEventListener('mousemove', drag_mousemove, true);
708 window.addEventListener('mouseup', drag_mouseup, true);
711 function ungrab_all() {
712 for (let tpiece of Object.keys(pieces)) {
713 let tp = pieces[tpiece]!;
715 set_ungrab(tpiece,tp);
716 api_piece(api, 'ungrab', tpiece,tp, { });
721 function set_grab(piece: PieceId, p: PieceInfo, owner: PlayerId) {
723 redisplay_ancillaries(piece,p);
724 recompute_keybindings();
726 function set_ungrab(piece: PieceId, p: PieceInfo) {
728 redisplay_ancillaries(piece,p);
729 recompute_keybindings();
732 function clear_halo(piece: PieceId, p: PieceInfo) {
733 p.last_seen_moved = null;
734 redisplay_ancillaries(piece,p);
737 function ancillary_node(piece: PieceId, stroke: string): SVGGraphicsElement {
738 var nelem = document.createElementNS(svg_ns,'use');
739 nelem.setAttributeNS(null,'href','#surround'+piece);
740 nelem.setAttributeNS(null,'stroke',stroke);
741 nelem.setAttributeNS(null,'fill','none');
745 function redisplay_ancillaries(piece: PieceId, p: PieceInfo) {
746 let href = '#surround'+piece;
747 console.log('REDISPLAY ANCILLARIES',href);
749 for (let celem = p.pelem.firstElementChild;
752 var nextelem = celem.nextElementSibling
753 let thref = celem.getAttributeNS(null,"href");
759 let halo_colour = null;
760 if (p.cseq_updatesvg != null) {
761 halo_colour = 'purple';
762 } else if (p.last_seen_moved != null) {
763 halo_colour = 'yellow';
764 } else if (p.held != null && p.pinned) {
765 halo_colour = '#8cf';
767 if (halo_colour != null) {
768 let nelem = ancillary_node(piece, halo_colour);
769 if (p.held != null) {
770 nelem.setAttributeNS(null,'stroke-width','2px');
772 p.pelem.prepend(nelem);
774 if (p.held != null) {
775 let da = players[p.held!]!.dasharray;
776 let nelem = ancillary_node(piece, 'black');
777 nelem.setAttributeNS(null,'stroke-dasharray',da);
778 p.pelem.appendChild(nelem);
782 function drag_mousemove(e: MouseEvent) {
783 var ctm = space.getScreenCTM()!;
784 var ddx = (e.clientX - dcx!)/(ctm.a * firefox_bug_zoom_factor_compensation);
785 var ddy = (e.clientY - dcy!)/(ctm.d * firefox_bug_zoom_factor_compensation);
786 var ddr2 = ddx*ddx + ddy*ddy;
787 if (!(dragging & DRAGGING.YES)) {
788 if (ddr2 > DRAGTHRESH) {
789 dragging |= DRAGGING.YES;
792 //console.log('mousemove', ddx, ddy, dragging);
793 if (dragging & DRAGGING.YES) {
794 console.log('DRAG PIECES',drag_pieces);
795 for (let dp of drag_pieces) {
796 console.log('DRAG PIECES PIECE',dp);
797 let tpiece = dp.piece;
798 let tp = pieces[tpiece]!;
799 var x = Math.round(dp.dox + ddx);
800 var y = Math.round(dp.doy + ddy);
801 tp.uelem.setAttributeNS(null, "x", x+"");
802 tp.uelem.setAttributeNS(null, "y", y+"");
804 api_piece(api_delay, 'm', tpiece,tp, [x, y] );
806 if (!(dragging & DRAGGING.RAISED) && drag_pieces.length==1) {
807 let dp = drag_pieces[0];
808 let piece = dp.piece;
809 let p = pieces[piece]!;
810 let dragraise = +p.pelem.dataset.dragraise!;
811 if (dragraise > 0 && ddr2 >= dragraise*dragraise) {
812 dragging |= DRAGGING.RAISED;
813 console.log('CHECK RAISE ', dragraise, dragraise*dragraise, ddr2);
814 piece_set_zlevel(piece,p, (oldtop_piece) => {
815 let oldtop_p = pieces[oldtop_piece]!;
816 let z = wasm_bindgen.increment(oldtop_p.z);
818 api_piece(api, "setz", piece,p, { z: z });
826 function drag_mouseup(e: MouseEvent) {
827 console.log('mouseup', dragging);
828 let ddr2 : number = drag_mousemove(e);
832 function drag_end() {
833 if (dragging == DRAGGING.MAYBE_UNGRAB ||
834 (dragging & ~DRAGGING.RAISED) == (DRAGGING.MAYBE_GRAB | DRAGGING.YES)) {
835 let dp = drag_pieces[0]!;
836 let piece = dp.piece;
837 let p = pieces[piece]!;
839 api_piece(api, 'ungrab', piece,p, { });
844 function drag_cancel() {
845 window.removeEventListener('mousemove', drag_mousemove, true);
846 window.removeEventListener('mouseup', drag_mouseup, true);
847 dragging = DRAGGING.NO;
851 // ----- general -----
853 messages.AddPlayer = <MessageHandler>function
854 (j: { player: string, data: PlayerInfo }) {
855 players[j.player] = j.data;
858 messages.RemovePlayer = <MessageHandler>function
859 (j: { player: string }) {
860 delete players[j.player];
863 messages.SetLinks = <MessageHandler>function
865 if (msg.length != 0 && layout == 'Portrait') {
868 links_elem.innerHTML = msg
873 messages.Log = <MessageHandler>function
874 (j: { when: string, logent: { html: string } }) {
875 add_timestamped_log_message(j.when, j.logent.html);
878 function add_log_message(msg_html: string) {
879 add_timestamped_log_message('', msg_html);
882 function add_timestamped_log_message(ts_html: string, msg_html: string) {
883 var lastent = log_elem.lastElementChild;
887 // https://stackoverflow.com/questions/487073/how-to-check-if-element-is-visible-after-scrolling/21627295#21627295
889 // https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API
891 let le_top = lastent.getBoundingClientRect()!.top;
892 let le_bot = lastent.getBoundingClientRect()!.bottom;
893 let ld_bot = logscroll_elem.getBoundingClientRect()!.bottom;
894 console.log("ADD_LOG_MESSAGE bboxes: le t b, bb",
895 le_top, le_bot, ld_bot);
896 return 0.5 * (le_bot + le_top) > ld_bot;
899 console.log('ADD LOG MESSAGE ',in_scrollback, layout, msg_html);
901 var ne : HTMLElement;
903 function add_thing(elemname: string, cl: string, html: string) {
904 var ie = document.createElement(elemname);
906 ie.setAttribute("class", cl);
910 if (layout == 'Portrait') {
911 ne = document.createElement('tr');
912 add_thing('td', 'logmsg', msg_html);
913 add_thing('td', 'logts', ts_html);
914 } else if (layout == 'Landscape') {
915 ts_html = last_log_ts.update(ts_html);
916 ne = document.createElement('div');
917 add_thing('span', 'logts', ts_html);
918 ne.appendChild(document.createElement('br'));
919 add_thing('span', 'logmsg', msg_html);
920 ne.appendChild(document.createElement('br'));
922 throw 'bad layout ' + layout;
924 log_elem.appendChild(ne);
926 if (!in_scrollback) {
927 logscroll_elem.scrollTop = logscroll_elem.scrollHeight;
933 function zoom_pct (): number | undefined {
934 let str = zoom_val.value;
935 let val = parseFloat(str);
943 function zoom_enable() {
944 zoom_btn.disabled = (zoom_pct() === undefined);
947 function zoom_activate() {
948 let pct = zoom_pct();
949 if (pct !== undefined) {
950 let fact = pct * 0.01;
951 let last_ctm_a = space.getScreenCTM()!.a;
952 (document.getElementsByTagName('body')[0] as HTMLElement)
953 .style.transform = 'scale('+fact+','+fact+')';
954 if (fact != last_zoom_factor) {
955 if (last_ctm_a == space.getScreenCTM()!.a) {
956 console.log('FIREFOX GETSCREENCTM BUG');
957 firefox_bug_zoom_factor_compensation = fact;
959 console.log('No firefox getscreenctm bug');
960 firefox_bug_zoom_factor_compensation = 1.0;
962 last_zoom_factor = fact;
965 zoom_btn.disabled = true;
968 // ----- test counter, startup -----
970 messages.Piece = <MessageHandler>function
971 (j: { piece: PieceId, op: Object }) {
972 console.log('PIECE UPDATE ',j)
974 var m = j.op as { [k: string]: Object };
975 var k = Object.keys(m)[0];
976 let p = pieces[piece]!;
977 pieceops[k](piece,p, m[k]);
980 type PieceStateMessage = {
987 uos: UoDescription[],
990 pieceops.ModifyQuiet = <PieceHandler>function
991 (piece: PieceId, p: PieceInfo, info: PieceStateMessage) {
992 console.log('PIECE UPDATE MODIFY QUIET ',piece,info)
993 piece_modify(piece, p, info, false);
996 pieceops.Modify = <PieceHandler>function
997 (piece: PieceId, p: PieceInfo, info: PieceStateMessage) {
998 console.log('PIECE UPDATE MODIFY LOuD ',piece,info)
999 piece_note_moved(piece,p);
1000 piece_modify(piece, p, info, false);
1003 piece_error_handlers.PosOffTable = <PieceErrorHandler>function()
1005 piece_error_handlers.Conflict = <PieceErrorHandler>function()
1008 function piece_modify(piece: PieceId, p: PieceInfo, info: PieceStateMessage,
1009 conflict_expected: boolean) {
1010 p.delem.innerHTML = info.svg;
1011 p.pelem= piece_element('piece',piece)!;
1012 p.uelem.setAttributeNS(null, "x", info.pos[0]+"");
1013 p.uelem.setAttributeNS(null, "y", info.pos[1]+"");
1015 p.pinned = info.pinned;
1017 piece_set_zlevel(piece,p, (oldtop_piece)=>{
1021 piece_checkconflict_nrda(piece,p,conflict_expected);
1022 redisplay_ancillaries(piece,p);
1023 recompute_keybindings();
1024 console.log('MODIFY DONE');
1028 pieceops.Insert = <PieceHandler>function
1029 (piece: PieceId, p: null,
1030 info: { svg: string, held: PlayerId, pos: Pos, z: number, zg: Generation}) {
1031 console.log('PIECE UPDATE INSERT ',piece,info)
1032 delem = document.createElementNS(svg_ns,'defs');
1033 delem.setAttributeNS(null,'id','defs'+piece);
1034 delem.innerHTML = info.svg;
1035 space.appendChild(delem);
1038 nelem.setAttributeNS(null,'stroke',stroke);
1039 nelem.setAttributeNS(null,'fill','none');
1042 function piece_set_zlevel(piece: PieceId, p: PieceInfo,
1043 modify : (oldtop_piece: PieceId) => void) {
1044 // Calls modify, which should set .z and/or .gz, and/or
1045 // make any necessary API call.
1047 // Then moves uelem to the right place in the DOM. This is done
1048 // by assuming that uelem ought to go at the end, so this is
1049 // O(new depth), which is right (since the UI for inserting
1050 // an object is itself O(new depth) UI operations to prepare.
1052 let oldtop_elem = (defs_marker.previousElementSibling! as
1053 unknown as SVGGraphicsElement);
1054 let oldtop_piece = oldtop_elem.dataset.piece!;
1055 modify(oldtop_piece);
1057 let ins_before = defs_marker
1059 for (; ; ins_before = earlier_elem) {
1060 earlier_elem = (ins_before.previousElementSibling! as
1061 unknown as SVGGraphicsElement);
1062 if (earlier_elem == pieces_marker) break;
1063 if (earlier_elem == p.uelem) continue;
1064 let earlier_p = pieces[earlier_elem.dataset.piece!]!;
1065 if (!piece_z_before(p, earlier_p)) break;
1067 if (ins_before != p.uelem)
1068 space.insertBefore(p.uelem, ins_before);
1071 function piece_note_moved(piece: PieceId, p: PieceInfo) {
1072 let now = performance.now();
1074 let need_redisplay = p.last_seen_moved == null;
1075 p.last_seen_moved = now;
1076 if (need_redisplay) redisplay_ancillaries(piece,p);
1078 let cutoff = now-1000.;
1079 while (movements.length > 0 && movements[0].this_motion < cutoff) {
1080 let mr = movements.shift()!;
1081 if (mr.p.last_seen_moved != null &&
1082 mr.p.last_seen_moved < cutoff) {
1083 mr.p.last_seen_moved = null;
1084 redisplay_ancillaries(mr.piece,mr.p);
1088 movements.push({ piece: piece, p: p, this_motion: now });
1091 function piece_z_before(a: PieceInfo, b: PieceInfo) {
1092 if (a.z < b.z ) return true;
1093 if (a.z > b.z ) return false;
1094 if (a.zg < b.zg) return true;
1095 if (a.zg > b.zg) return false;
1099 pieceops.Move = <PieceHandler>function
1100 (piece,p, info: Pos ) {
1101 piece_checkconflict_nrda(piece,p,false);
1102 piece_note_moved(piece, p);
1104 p.uelem.setAttributeNS(null, "x", info[0]+"");
1105 p.uelem.setAttributeNS(null, "y", info[1]+"");
1108 pieceops.SetZLevel = <PieceHandler>function
1109 (piece,p, info: { z: ZCoord, zg: Generation }) {
1110 piece_note_moved(piece,p);
1111 piece_set_zlevel(piece,p, (oldtop_piece)=>{
1112 let oldtop_p = pieces[oldtop_piece]!;
1118 messages.Recorded = <MessageHandler>function
1119 (j: { piece: PieceId, cseq: ClientSeq, gen: Generation
1120 zg: Generation|null, svg: string | null } ) {
1121 let piece = j.piece;
1122 let p = pieces[piece]!;
1123 if (p.cseq != null && j.cseq >= p.cseq) {
1126 if (p.cseq_updatesvg != null && j.cseq >= p.cseq_updatesvg) {
1127 p.cseq_updatesvg = null;
1128 redisplay_ancillaries(piece,p);
1130 if (j.svg != null) {
1131 p.delem.innerHTML = j.svg;
1132 p.pelem= piece_element('piece',piece)!;
1133 redisplay_ancillaries(piece,p);
1136 var zg_new = j.zg; // type narrowing doesn't propagate :-/
1137 piece_set_zlevel(piece,p, (oldtop_piece: PieceId)=>{
1144 messages.Error = <MessageHandler>function
1146 console.log('ERROR UPDATE ', m);
1147 var k = Object.keys(m)[0];
1148 update_error_handlers[k](m[k]);
1151 type PieceOpError = {
1154 state: PieceStateMessage,
1157 update_error_handlers.PieceOpError = <MessageHandler>function
1159 let p = pieces[m.piece];
1160 if (p == null) return;
1161 let conflict_expected = piece_error_handlers[m.error](m.piece, p, m);
1162 piece_modify(m.piece, p, m.state, conflict_expected);
1165 function piece_checkconflict_nrda(piece: PieceId, p: PieceInfo,
1166 conflict_expected: boolean): boolean {
1167 if (p.cseq != null) {
1169 if (drag_pieces.some(function(dp) { return dp.piece == piece; })) {
1170 console.log('drag end due to conflict');
1173 if (!conflict_expected) {
1174 add_log_message('Conflict! - simultaneous update');
1180 function test_swap_stack() {
1181 let old_bot = pieces_marker.nextElementSibling!;
1182 let container = old_bot.parentElement!;
1183 container.insertBefore(old_bot, defs_marker);
1184 window.setTimeout(test_swap_stack, 1000);
1187 function startup() {
1188 console.log('STARTUP');
1189 console.log(wasm_bindgen.setup("OK"));
1191 var body = document.getElementById("main-body")!;
1192 zoom_btn = document.getElementById("zoom-btn") as any;
1193 zoom_val = document.getElementById("zoom-val") as any;
1194 links_elem = document.getElementById("links") as any;
1195 ctoken = body.dataset.ctoken!;
1196 us = body.dataset.us!;
1197 gen = +body.dataset.gen!;
1198 let sse_url_prefix = body.dataset.sseUrlPrefix!;
1199 status_node = document.getElementById('status')!;
1200 status_node.innerHTML = 'js-done';
1201 log_elem = document.getElementById("log")!;
1202 logscroll_elem = document.getElementById("logscroll") || log_elem;
1203 let dataload = JSON.parse(body.dataset.load!);
1204 players = dataload.players!;
1205 delete body.dataset.load;
1206 uos_node = document.getElementById("uos")!;
1208 space = svg_element('space')!;
1209 pieces_marker = svg_element("pieces_marker")!;
1210 defs_marker = svg_element("defs_marker")!;
1211 svg_ns = space.getAttribute('xmlns')!;
1213 for (let uelem = pieces_marker.nextElementSibling! as SVGGraphicsElement;
1214 uelem != defs_marker;
1215 uelem = uelem.nextElementSibling! as SVGGraphicsElement) {
1216 let piece = uelem.dataset.piece!;
1217 let p = JSON.parse(uelem.dataset.info!);
1219 p.delem = piece_element('defs',piece);
1220 p.pelem = piece_element('piece',piece);
1222 delete uelem.dataset.info;
1224 redisplay_ancillaries(piece,p);
1227 last_log_ts = wasm_bindgen.timestamp_abbreviator(dataload.last_log_ts);
1229 var es = new EventSource(
1230 sse_url_prefix + "/_/updates?ctoken="+ctoken+'&gen='+gen
1232 es.onmessage = function(event) {
1233 console.log('GOTEVE', event.data);
1237 var [tgen, ms] = JSON.parse(event.data);
1239 k = Object.keys(m)[0];
1245 var s = exc.toString();
1246 string_report_error('exception handling update '
1247 + k + ': ' + JSON.stringify(m) + ': ' + s);
1250 es.addEventListener('commsworking', function(event) {
1251 console.log('GOTDATA', (event as any).data);
1252 status_node.innerHTML = (event as any).data;
1254 es.addEventListener('player-gone', function(event) {
1255 console.log('PLAYER-GONE', event);
1256 status_node.innerHTML = (event as any).data;
1257 add_log_message('<strong>You are no longer in the game</strong>');
1258 space.removeEventListener('mousedown', some_mousedown);
1259 document.removeEventListener('keydown', some_keydown);
1262 es.addEventListener('updates-expired', function(event) {
1263 console.log('UPDATES-EXPIRED', event);
1264 string_report_error('connection to server interrupted too long');
1266 es.onerror = function(e) {
1267 console.log('FOO',e,es);
1270 updates_event_source : es,
1271 updates_event_source_ready : es.readyState,
1272 update_oe : (e as any).className,
1275 recompute_keybindings();
1276 space.addEventListener('mousedown', some_mousedown);
1277 document.addEventListener('keydown', some_keydown);
1280 declare var wasm_input : any;
1281 var wasm_promise : Promise<any>;;
1284 console.log('DOLOAD');
1285 globalinfo_elem = document.getElementById('global-info')!;
1286 layout = globalinfo_elem!.dataset!.layout! as any;
1287 var elem = document.getElementById('loading_token')!;
1288 var ptoken = elem.dataset.ptoken;
1289 xhr_post_then('/_/session/' + layout,
1290 JSON.stringify({ ptoken : ptoken }),
1293 wasm_promise = wasm_input
1294 .then(wasm_bindgen);
1297 function loaded(xhr: XMLHttpRequest){
1298 console.log('LOADED');
1299 var body = document.getElementById('loading_body')!;
1300 wasm_promise.then((got_wasm) => {
1302 body.outerHTML = xhr.response;
1307 // todo scroll of log messages to bottom did not always work somehow
1308 // think I have fixed this with approximation