chiark / gitweb /
dc279f2ea8280a4980a23b37efbc29a164a062f9
[otter.git] / templates / script.ts
1 // -*- JavaScript -*-
2
3 // Copyright 2020-2021 Ian Jackson and contributors to Otter
4 // SPDX-License-Identifier: AGPL-3.0-or-later
5 // There is NO WARRANTY.
6
7 // elemnts for a piece
8 //
9 // In svg toplevel
10 //
11 //   uelem
12 //      #use{}
13 //      <use id="use{}", href="#piece{}" x= y= >
14 //         .piece   piece id (static)
15 //      container to allow quick movement and hang stuff off
16 //
17 //   delem
18 //      #defs{}
19 //      <def id="defs{}">
20 //
21 // And in each delem
22 //
23 //   pelem
24 //   #piece{}
25 //         .dragraise   dragged more than this ?  raise to top!
26 //         .special     enum RenderSpecial
27 //      <g id="piece{}" >
28 //      currently-displayed version of the piece
29 //      to allow addition/removal of selected indication
30 //      contains 1 or 3 subelements:
31 //      one is straight from server and not modified
32 //      one is possible <use href="#select{}" >
33 //      one is possible <use href="#halo{}" >
34 //
35 //   #select{}
36 //      generated by server, referenced by JS in pelem for selection
37 //
38 //   #def.{}.stuff
39 //      generated by server, reserved for Piece trait impl
40
41 type PieceId = string;
42 type PlayerId = string;
43 type Pos = [number, number];
44 type Rect = [Pos, Pos];
45 type ClientSeq = number;
46 type Generation = number;
47 type UoKind = 'Client' | "Global"| "Piece" | "ClientExtra" | "GlobalExtra";
48 type WhatResponseToClientOp = "Predictable" | "Unpredictable" | "UpdateSvg";
49 type HeldUsRaising = "NotYet" | "Lowered" | "Raised"
50 type Timestamp = number; // unix time_t, will break in 285My
51 type Layout = 'Portrait' | 'Landscape';
52 type PieceMoveable = "No" | "IfWresting" | "Yes";
53 type CompassAngle = number;
54 type FaceId = number;
55
56 type UoDescription = {
57   kind: UoKind;
58   wrc: WhatResponseToClientOp,
59   def_key: string,
60   opname: string,
61   desc: string,
62 }
63
64 type UoRecord = UoDescription & {
65   targets: PieceId[] | null,
66 }
67
68 type ZCoord = string;
69
70 // On load, starts from SessionPieceLoadJson (Rust-only)
71 // On update, updated field-by-field from PreparedPieceState (Rust&JS)
72 type PieceInfo = {
73   held : PlayerId | null,
74   cseq_main : number | null,
75   cseq_loose: number | null,
76   cseq_updatesvg : number | null,
77   z : ZCoord,
78   zg : Generation,
79   angle: CompassAngle,
80   pinned: boolean,
81   moveable: PieceMoveable,
82   rotateable: boolean,
83   multigrab: boolean,
84   desc: string,
85   uos : UoDescription[],
86   uelem : SVGGraphicsElement,
87   delem : SVGGraphicsElement,
88   pelem : SVGGraphicsElement,
89   queued_moves : number,
90   last_seen_moved : DOMHighResTimeStamp | null, // non-0 means halo'd
91   held_us_inoccult: boolean,
92   held_us_raising: HeldUsRaising,
93   bbox: Rect,
94   drag_delta: number,
95   special: SpecialRendering | null,
96 }
97
98 type SpecialRendering = {
99   kind: string,
100   stop: SpecialRenderingCallback,
101 }
102
103 let wasm : InitOutput;
104
105 var pieces : { [piece: string]: PieceInfo } = Object.create(null);
106
107 type MessageHandler = (op: Object) => void;
108 type PieceHandler = (piece: PieceId, p: PieceInfo, info: Object) => void;
109 type PieceErrorHandler = (piece: PieceId, p: PieceInfo, m: PieceOpError)
110   => boolean;
111 type SpecialRenderingCallback =
112   (piece: PieceId, p: PieceInfo, s: SpecialRendering) => void;
113 interface DispatchTable<H> { [key: string]: H };
114
115 var otter_debug: boolean;
116
117 // from header
118 var movehist_len_i: number;
119 var movehist_len_max: number;
120 var movehist_lens: number[];
121
122 // todo turn all var into let
123 // todo any exceptions should have otter in them or something
124 var globalinfo_elem : HTMLElement;
125 var layout: Layout;
126 var held_surround_colour: string;
127 var general_timeout : number = 10000;
128 var messages : DispatchTable<MessageHandler> = Object();
129 var special_renderings : DispatchTable<SpecialRenderingCallback> = Object();
130 var pieceops : DispatchTable<PieceHandler> = Object();
131 var update_error_handlers : DispatchTable<MessageHandler> = Object();
132 var piece_error_handlers : DispatchTable<PieceErrorHandler> = Object();
133 var our_dnd_type = "text/puvnex-game-server-dummy";
134 var api_queue : [string, Object][] = [];
135 var api_posting = false;
136 var us : PlayerId;
137 var gen : Generation = 0;
138 var cseq : ClientSeq = 0;
139 var ctoken : string;
140 var uo_map : { [k: string]: UoRecord | null } = Object.create(null);
141 var keyops_local : { [opname: string]: (uo: UoRecord) => void } = Object();
142 var last_log_ts: wasm_bindgen.TimestampAbbreviator;
143 var last_zoom_factor : number = 1.0;
144 var firefox_bug_zoom_factor_compensation : number = 1.0;
145 var test_update_hook : () => void;
146
147 var svg_ns : string;
148 var space : SVGGraphicsElement;
149 var pieces_marker : SVGGraphicsElement;
150 var defs_marker : SVGGraphicsElement;
151 var movehist_start: SVGGraphicsElement;
152 var movehist_end: SVGGraphicsElement;
153 var rectsel_path: SVGGraphicsElement;
154 var log_elem : HTMLElement;
155 var logscroll_elem : HTMLElement;
156 var status_node : HTMLElement;
157 var uos_node : HTMLElement;
158 var zoom_val : HTMLInputElement;
159 var zoom_btn : HTMLInputElement;
160 var links_elem : HTMLElement;
161 var was_wresting: boolean;
162 var wresting: boolean;
163 var occregions: wasm_bindgen.RegionList;
164 let special_count: number | null;
165
166 var movehist_gen: number = 0;
167 const MOVEHIST_ENDS = 2.5;
168 const SPECIAL_MULTI_DELTA_EACH = 3;
169 const SPECIAL_MULTI_DELTA_MAX = 18;
170
171 type PaneName = string;
172 const pane_keys : { [key: string]: PaneName } = {
173   "H" : "help",
174   "U" : "players",
175   "B" : "bundles",
176 };
177
178 const uo_kind_prec : { [kind: string]: number } = {
179   'GlobalExtra' :  50,
180   'Client'      :  70,
181   'Global'      : 100,
182   'Piece'       : 200,
183   'ClientExtra' : 500,
184 }
185
186 type PlayerInfo = {
187   dasharray : string,
188   nick: string,
189 }
190 var players : { [player: string]: PlayerInfo };
191
192 type MovementRecord = { // for yellow halo, unrelasted to movehist
193   piece: PieceId,
194   p: PieceInfo,
195   this_motion: DOMHighResTimeStamp,
196 }
197 var movements : MovementRecord[] = [];
198
199 function xhr_post_then(url : string, data: string,
200                        good : (xhr: XMLHttpRequest) => void) {
201   var xhr : XMLHttpRequest = new XMLHttpRequest();
202   xhr.onreadystatechange = function(){
203     if (xhr.readyState != XMLHttpRequest.DONE) { return; }
204     if (xhr.status != 200) { xhr_report_error(xhr); }
205     else { good(xhr); }
206   };
207   xhr.timeout = general_timeout;
208   xhr.open('POST',url);
209   xhr.setRequestHeader('Content-Type','application/json');
210   xhr.send(data);
211 }
212
213 function xhr_report_error(xhr: XMLHttpRequest) {
214   json_report_error({
215     statusText : xhr.statusText,
216     responseText : xhr.responseText,
217   });
218 }
219
220 function json_report_error(error_for_json: Object) {
221   let error_message = JSON.stringify(error_for_json);
222   string_report_error(error_message);
223 }
224
225 function string_report_error(error_message: String) {
226   string_report_error_raw('Error (reloading may help?): ' + error_message)
227 }
228 function string_report_error_raw(error_message: String) {
229   let errornode = document.getElementById('error')!;
230   errornode.textContent += '\n' + error_message;
231   console.error("ERROR reported via log", error_message);
232   // todo want to fix this for at least basic game reconfigs, auto-reload?
233 }
234
235 function api_immediate(meth: string, data: Object) {
236   api_queue.push([meth, data]);
237   api_check();
238 }
239 function api_delay(meth: string, data: Object) {
240   if (api_queue.length==0) window.setTimeout(api_check, 10);
241   api_queue.push([meth, data]);
242 }
243 function api_check() {
244   if (api_posting) { return; }
245   if (!api_queue.length) { test_update_hook(); return; }
246   do {
247     var [meth, data] = api_queue.shift()!;
248     if (meth != 'm') break;
249     let piece = (data as any).piece;
250     let p = pieces[piece];
251     if (p == null) break;
252     p.queued_moves--;
253     if (p.queued_moves == 0) break;
254   } while (api_queue.length);
255   api_posting = true;
256   xhr_post_then('/_/api/'+meth, JSON.stringify(data), api_posted);
257 }
258 function api_posted() {
259   api_posting = false;
260   api_check();
261 }
262
263 function api_piece_x(f: (meth: string, payload: Object) => void,
264                      loose: boolean,
265                      meth: string,
266                      piece: PieceId, p: PieceInfo,
267                      op: Object) {
268   clear_halo(piece,p);
269   cseq += 1;
270   if (loose) {
271     p.cseq_loose = cseq;
272   } else {
273     p.cseq_main = cseq;
274     p.cseq_loose = null;
275   }
276   f(meth, {
277     ctoken : ctoken,
278     piece : piece,
279     gen : gen,
280     cseq : cseq,
281     op : op,
282     loose: loose,
283   })
284 }
285 function api_piece(meth: string,
286                    piece: PieceId, p: PieceInfo,
287                    op: Object) {
288   api_piece_x(api_immediate, false,meth, piece, p, op);
289 }
290
291 function svg_element(id: string): SVGGraphicsElement | null {
292   let elem = document.getElementById(id);
293   return elem as unknown as (SVGGraphicsElement | null);
294 }
295 function piece_element(base: string, piece: PieceId): SVGGraphicsElement | null
296 {
297   return svg_element(base+piece);
298 }
299
300 function piece_moveable(p: PieceInfo) {
301   return p.moveable == 'Yes' || p.moveable == 'IfWresting' && wresting;
302 }
303
304 // ----- key handling -----
305
306 function recompute_keybindings() {
307   uo_map = Object.create(null);
308   let all_targets = [];
309   for (let piece of Object.keys(pieces)) {
310     let p = pieces[piece];
311     if (p.held != us) continue;
312     all_targets.push(piece);
313     for (var uo of p.uos) {
314       let currently = uo_map[uo.def_key];
315       if (currently === null) continue;
316       if (currently !== undefined) {
317         if (currently.opname != uo.opname) {
318           uo_map[uo.def_key] = null;
319           continue;
320         }
321       } else {
322         currently = {
323           targets: [],
324           ...uo
325         };
326         uo_map[uo.def_key] = currently;
327       }
328       currently.desc = currently.desc < uo.desc ? currently.desc : uo.desc;
329       currently.targets!.push(piece);
330     }
331   }
332   all_targets.sort(pieceid_z_cmp);
333   let add_uo = function(targets: PieceId[] | null, uo: UoDescription) {
334     uo_map[uo.def_key] = {
335       targets: targets,
336       ...uo
337     };
338   };
339   if (all_targets.length) {
340     let got_rotateable = false;
341     for (let t of all_targets) {
342       if (pieces[t]!.rotateable)
343         got_rotateable = true;
344     }
345     if (got_rotateable) {
346       add_uo(all_targets, {
347         def_key: 'l',
348         kind: 'Client',
349         wrc: 'Predictable',
350         opname: "left",
351         desc: "rotate left",
352       });
353       add_uo(all_targets, {
354         def_key: 'r',
355         kind: 'Client',
356         wrc: 'Predictable',
357         opname: "right",
358         desc: "rotate right",
359       });
360     }
361     add_uo(all_targets, {
362       def_key: 'b',
363       kind: 'Client',
364       wrc: 'Predictable',
365       opname: "lower",
366       desc: "send to bottom (below other pieces)",
367     });
368     add_uo(all_targets, {
369       def_key: 't',
370       kind: 'Client',
371       wrc: 'Predictable',
372       opname: "raise",
373       desc: "raise to top",
374     });
375   }
376   if (all_targets.length) {
377     let got = 0;
378     for (let t of all_targets) {
379       got |= 1 << Number(pieces[t]!.pinned);
380     }
381     if (got == 1) {
382       add_uo(all_targets, {
383         def_key: 'P',
384         kind: 'ClientExtra',
385         opname: 'pin',
386         desc: 'Pin to table',
387         wrc: 'Predictable',
388       });
389     } else if (got == 2) {
390       add_uo(all_targets, {
391         def_key: 'P',
392         kind: 'ClientExtra',
393         opname: 'unpin',
394         desc: 'Unpin from table',
395         wrc: 'Predictable',
396       });
397     }
398   }
399   add_uo(null, {
400     def_key: wresting ? 'W SPC' /* won't match, handle ad-hoc */ : 'W',
401     kind: 'ClientExtra',
402     opname: 'wrest',
403     desc: wresting ? 'Exit wresting mode' : 'Enter wresting mode',
404     wrc: 'Predictable',
405   });
406   if (special_count != null) {
407     let desc;
408     if (special_count == 0) {
409       desc = 'select bottommost';
410     } else {
411       desc = `select ${special_count}`;
412     }
413     desc = `cancel <strong style="color:purple">${desc}</strong>`;
414     add_uo(null, {
415       def_key: 'SPC', // won't match key event; we handle this ad-hoc
416       kind: 'ClientExtra',
417       opname: 'cancel-special',
418       desc: desc,
419       wrc: 'Predictable',
420     });
421   }
422   add_uo(null, {
423     def_key: 'h',
424     kind: 'ClientExtra',
425     opname: 'motion-hint-history',
426     desc: 'Recent history display',
427     wrc: 'Predictable',
428   });
429   var uo_keys = Object.keys(uo_map);
430   uo_keys.sort(function (ak,bk) {
431     let a = uo_map[ak];
432     let b = uo_map[bk];
433     if (a==null || b==null) return (
434       ( (!!a) as any ) -
435       ( (!!b) as any )
436     );
437     return uo_kind_prec[a.kind] - uo_kind_prec[b.kind]
438       || ak.localeCompare(bk);
439   });
440   let mid_elem = null;
441   for (let celem = uos_node.firstElementChild;
442        celem != null;
443        celem = nextelem) {
444     var nextelem = celem.nextElementSibling;
445     let cid = celem.getAttribute("id");
446     if (cid == "uos-mid") mid_elem = celem;
447     else if (celem.getAttribute("class") == 'uos-mid') { }
448     else celem.remove();
449   }
450   for (var kk of uo_keys) {
451     let uo = uo_map[kk];
452     if (!uo) continue;
453     let prec = uo_kind_prec[uo.kind];
454     let ent = document.createElement('div');
455     ent.innerHTML = '<b>' + kk + '</b> ' + uo.desc;
456     if (prec < 400) {
457       ent.setAttribute('class','uokey-l');
458       uos_node.insertBefore(ent, mid_elem);
459     } else {
460       ent.setAttribute('class','uokey-r');
461       uos_node.appendChild(ent);
462     }
463   }
464 }
465
466 function some_keydown(e: KeyboardEvent) {
467   // https://developer.mozilla.org/en-US/docs/Web/API/Document/keydown_event
468   // says to do this, something to do with CJK composition.
469   // https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent
470   // says that keyCode is deprecated
471   // my tsc says this isComposing thing doesn't exist.  wat.
472   if ((e as any).isComposing /* || e.keyCode === 229 */) return;
473   if (e.ctrlKey || e.altKey || e.metaKey) return;
474   if (e.target) {
475     // someone else is dealing with it ?
476     let tag = (e.target as HTMLElement).tagName;
477     if (tag == 'INPUT') return;
478   }
479
480   let y = function() { e.preventDefault(); e.stopPropagation(); }
481
482   let pane = pane_keys[e.key];
483   if (pane) {
484     y();
485     return pane_switch(pane);
486   }
487
488   let special_count_key = parseInt(e.key);
489   if (isFinite(special_count_key)) {
490     y();
491     if (special_count == null) special_count = 0;
492     special_count *= 10;
493     special_count += special_count_key;
494     special_count %= 100000;
495     mousecursor_etc_reupdate();
496     return;
497   }
498   if (e.key == ' ' || (e.key == 'W' && wresting)) {
499     y();
500     special_count = null;
501     wresting = false;
502     mousecursor_etc_reupdate();
503     return;
504   }
505   if (e.key == 'Backspace') {
506     if (special_count == null) {
507       wresting = false;
508     } else if (special_count >= 10) {
509       special_count = Math.round(special_count / 10 - .45);
510     } else {
511       special_count = null;
512     }
513     mousecursor_etc_reupdate();
514     return;
515   }
516
517   let uo = uo_map[e.key];
518   if (uo === undefined || uo === null) return;
519
520   y();
521   console.log('KEY UO', e, uo);
522   if (uo.kind == 'Client' || uo.kind == 'ClientExtra') {
523     let f = keyops_local[uo.opname];
524     f(uo);
525     return;
526   }
527   if (!(uo.kind == 'Global' || uo.kind == 'GlobalExtra' || uo.kind == 'Piece'))
528     throw 'bad kind '+uo.kind;
529
530   for (var piece of uo.targets!) {
531     let p = pieces[piece]!;
532     api_piece('k', piece, p, { opname: uo.opname, wrc: uo.wrc });
533     if (uo.wrc == 'UpdateSvg') {
534       // No UpdateSvg is loose, so no need to check p.cseq_loose
535       p.cseq_updatesvg = p.cseq_main;
536       redisplay_ancillaries(piece,p);
537     }
538   }
539 }
540
541 function pane_switch(newpane: PaneName) {
542   let new_e;
543   for (;;) {
544     new_e = document.getElementById('pane_' + newpane)!;
545     let style = new_e.getAttribute('style');
546     if (style || newpane == 'help') break;
547     newpane = 'help';
548   }
549   for (let old_e = new_e.parentElement!.firstElementChild;
550        old_e;
551        old_e = old_e.nextElementSibling) {
552     old_e.setAttribute('style','display: none;');
553   }
554   new_e.removeAttribute('style');
555 }
556
557 function mousecursor_etc_reupdate() {
558   let style_elem = document.getElementById("space-cursor-style")!;
559   let style_text;
560   let svg;
561   let xy;
562   let path = 'stroke-linecap="square" d="M -10 -10 10 10 M 10 -10 -10 10"';
563
564   document.getElementById('wresting-warning')!.innerHTML = !wresting ? "" :
565     " <strong>(wresting mode!)</strong>";
566
567   if (wresting != was_wresting) {
568     ungrab_all();
569     was_wresting = wresting;
570   }
571   
572   if (wresting) {
573     let text;
574     if (special_count == null) {
575       text = "WREST";
576     } else if (special_count == 0) {
577       text = "W v";
578     } else {
579       text = "W " + special_count;
580     }
581     xy = '60 15';
582     svg =
583 `<svg xmlns="http://www.w3.org/2000/svg"
584      viewBox="-60 -15 120 60" width="120" height="60">
585   <g transform="translate(0 0)">
586     <path stroke-width="8" stroke="black" ${path}/>
587     <path stroke-width="4" stroke="#cc0" ${path}/>
588  <text x="0" y="40" fill="black" stroke="#cc0" stroke-width="1.7" text-align="center" text-anchor="middle"
589  font-family="sans-serif" font-size="30">${text}</text>
590   </g></svg>`;
591   } else if (special_count == null) {
592   } else {
593     if (special_count != 0) {
594       let text_len = special_count.toString().length;
595       let text_x = text_len <= 3 ? 0 : -15;
596       let text_size = text_len <= 3 ? 50 : 45 * (4/text_len);
597       xy = '15 50';
598       svg = 
599 `<svg xmlns="http://www.w3.org/2000/svg"
600      viewBox="-15 0 120 65" width="120" height="65">
601   <g transform="translate(0 50)">
602     <path stroke-width="8" stroke="#fcf" ${path}/>
603     <path stroke-width="4" stroke="purple" ${path}/>
604     <text x="${text_x}" y="0" fill="purple" stroke="#fcf" stroke-width="2"
605        font-family="sans-serif" font-size="${text_size}">${special_count}</text>
606   </g></svg>`;
607     } else {
608       let path = 'stroke-linecap="square" d="M -10 -10 0 0 10 -10 M 0 0 0 -20"';
609       xy = '15 30';
610       svg =
611 `<svg xmlns="http://www.w3.org/2000/svg"
612      viewBox="-15 -25 30 30" width="30" height="30">
613   <g transform="translate(0 0)">
614     <path stroke-width="8" stroke="#fcf" ${path}/>
615     <path stroke-width="4" stroke="purple" ${path}/>
616   </g></svg>`;
617     }
618   }
619   // Empirically, setting this to '' and then back to the SVG data
620   // seems to cause Firefox to update it more promptly.
621   style_elem.innerHTML = '';
622   if (svg !== undefined) {
623     let svg_data = btoa(svg);
624     style_text =
625 `svg[id=space] {
626   cursor: url(data:image/svg+xml;base64,${svg_data}) ${xy}, auto;
627 }`;
628     style_elem.innerHTML = style_text;
629   }
630   recompute_keybindings();
631 }
632
633 keyops_local['left' ] = function (uo: UoRecord) { rotate_targets(uo, +1); }
634 keyops_local['right'] = function (uo: UoRecord) { rotate_targets(uo, -1); }
635
636 function rotate_targets(uo: UoRecord, dangle: number): boolean {
637   for (let piece of uo.targets!) {
638     let p = pieces[piece]!;
639     if (!p.rotateable) continue;
640     p.angle += dangle + 8;
641     p.angle %= 8;
642     let transform = wasm_bindgen.angle_transform(p.angle);
643     p.pelem.setAttributeNS(null,'transform',transform);
644     api_piece('rotate', piece,p, p.angle);
645   }
646   recompute_keybindings();
647   return true;
648 }
649
650 // ----- lower -----
651
652 type LowerTodoItem = {
653   piece: PieceId,
654   p: PieceInfo,
655   heavy: boolean,
656 };
657
658 type LowerTodoList = { [piece: string]: LowerTodoItem };
659
660 keyops_local['lower'] = function (uo: UoRecord) { lower_targets(uo); }
661
662 function lower_heavy(p: PieceInfo): boolean {
663   return wresting || p.pinned || p.moveable == "No";
664 }
665
666 function lower_targets(uo: UoRecord): boolean {
667   let targets_todo : LowerTodoList = Object.create(null);
668
669   for (let piece of uo.targets!) {
670     let p = pieces[piece]!;
671     let heavy = lower_heavy(p);
672     targets_todo[piece] = { p, piece, heavy, };
673   }
674   let problem = lower_pieces(targets_todo);
675   if (problem !== null) {
676     add_log_message('Cannot lower: ' + problem);
677     return false;
678   }
679   return true;
680 }
681
682 function lower_pieces(targets_todo: LowerTodoList):
683  string | null
684 {
685   // This is a bit subtle.  We don't want to lower below heavy pieces
686   // (unless we are heavy too, or the user is wresting).  But maybe
687   // the heavy pieces aren't already at the bottom.  For now we will
688   // declare that all heavy pieces "should" be below all light
689   // ones.  Not as an invariant, but as a thing we will do here to try
690   // to make a sensible result.  We implement this as follows: if we
691   // find heavy pieces above light pieces, we move those heavy
692   // pieces to the bottom too, just below us, preserving their
693   // relative order.
694   //
695   // Disregarding heavy targets:
696   //
697   // Z     <some stuff not including any light targets>
698   // Z
699   //       topmost light target           *
700   // B (
701   // B     light non-target
702   // B |   light target                   *
703   // B |   heavy non-target, mis-stacked  *
704   // B )*
705   // B
706   //       bottommost light non-target
707   //        if that is below topmost light target
708   //            <- tomove_light: insert targets from * here           Q ->
709   //            <- tomove_misstacked: insert non-targets from * here  Q ->
710   //            <- heavy non-targets with clashing Z Coords           X ->
711   // A
712   // A     heavy non-targets (nomove_heavy)
713   //            <- tomove_heavy: insert all heavy targets here        P ->
714   //
715   // When wresting, treat all targets as heavy.
716
717   type Entry = {
718     piece: PieceId,
719     p: PieceInfo,
720   };
721   // bottom of the stack order first
722   let tomove_light       : Entry[] = [];
723   let tomove_misstacked  : Entry[] = [];
724   let nomove_heavy       : Entry[] = [];
725   let tomove_heavy       : Entry[] = [];
726
727                                                     //  A      B      Z
728   let q_z_top : ZCoord | null = null;               //  null   some   some
729   let n_targets_todo_light = 0;                     //                0
730
731   let any_targets = false;
732   for (const piece of Object.keys(targets_todo)) {
733     any_targets = true;
734     let p = targets_todo[piece];
735     if (!p.heavy) n_targets_todo_light++;
736   }
737   if (!any_targets) return 'Nothing to lower!';
738
739   let walk = pieces_marker;
740   for (;;) { // starting at the bottom of the stack order
741     if (Object.keys(targets_todo).length == 0 &&
742        q_z_top !== null) {
743       // no targets left, state Z, we can stop now
744       console.log('LOWER STATE Z FINISHED');
745       break;
746     }
747
748     let new_walk = walk.nextElementSibling;
749     if (new_walk == null) {
750       console.log('LOWER WALK NO SIBLING!');
751       break;
752     }
753     walk = new_walk as SVGGraphicsElement;
754     let piece = walk.dataset.piece;
755     if (piece == null) {
756       console.log('LOWER WALK REACHED TOP');
757       break;
758     }
759
760     let p = pieces[piece]!;
761     let todo = targets_todo[piece];
762     if (todo) {
763       let xst = '';
764       if (q_z_top === null && !todo.heavy) {
765         q_z_top = p.z;
766         xst = 'STATE -> B';
767       }
768       console.log('LOWER WALK', piece, 'TODO', todo.heavy ? "H" : "_", xst);
769       delete targets_todo[piece];
770       if (!todo.heavy) n_targets_todo_light--;
771       (todo.heavy ? tomove_heavy : tomove_light).push(todo);
772       continue;
773     }
774
775     let p_heavy = lower_heavy(p);
776     if (q_z_top === null) { // state A
777       if (!p_heavy) {
778         console.log('LOWER WALK', piece, 'STATE A -> Z');
779         q_z_top = p.z;
780       } else {
781         console.log('LOWER WALK', piece, 'STATE A');
782         nomove_heavy.push({ p, piece });
783       }
784       continue;
785     }
786
787     // state B
788     if (p_heavy) {
789       console.log('LOWER WALK', piece, 'STATE B MIS-STACKED');
790       tomove_misstacked.push({ p, piece });
791     } else {
792       console.log('LOWER WALK', piece, 'STATE B');
793     }
794   }
795
796   if (q_z_top === null) {
797     // Somehow we didn't find the top of Q, so we didn't meet any
798     // targets.  (In the walk loop, we always set q_z_top if todo.)
799     q_z_top =
800       tomove_misstacked.length ? tomove_misstacked[0].p.z :
801       tomove_light     .length ? tomove_light     [0].p.z :
802                                  tomove_heavy     [0].p.z;
803   }
804
805   while (nomove_heavy.length &&
806          (tomove_light.length || tomove_misstacked.length) &&
807          nomove_heavy[nomove_heavy.length-1].p.z == q_z_top) {
808     // Yowzer.  We have to reset the Z coordinates on these heavy
809     // pieces, whose Z coordinate is the same as the stuff we are not
810     // touching, because otherwise there is no gap.
811     //
812     // Treating them as misstacked instead is sufficient, provided
813     // we put them at the front (bottom end) of the misstacked list.
814     //
815     // This is X in the chart.
816     //
817     let restack = nomove_heavy.pop()!;
818     console.log('LOWER CLASHING Z - RESTACKING', restack);
819     tomove_misstacked.unshift(restack);
820   }
821
822   type PlanEntry = {
823     content: Entry[], // bottom to top
824     z_top: ZCoord,
825     z_bot: ZCoord | null,
826   };
827
828   let plan : PlanEntry[] = [];
829
830   console.log('LOWER PARTQ X', tomove_misstacked);
831   console.log('LOWER PARTQ L', tomove_light);
832   console.log('LOWER PARTP H', tomove_heavy);
833   let partQ = tomove_misstacked.concat(tomove_light);
834   let partP = tomove_heavy;
835
836   if (nomove_heavy.length == 0) {
837     plan.push({
838       content: partP.concat(partQ),
839       z_top: q_z_top,
840       z_bot : null,
841     });
842   } else {
843     plan.push({
844       content: partQ,
845       z_top: q_z_top,
846       z_bot: nomove_heavy[nomove_heavy.length-1].p.z,
847     }, {
848       content: partP,
849       z_top: nomove_heavy[0].p.z,
850       z_bot: null,
851     });
852   }
853
854   console.log('LOWER PLAN', plan);
855
856   for (const pe of plan) {
857     for (const e of pe.content) {
858       if (e.p.held != null && e.p.held != us) {
859         return "lowering would disturb a piece held by another player";
860       }
861     }
862   }
863
864   for (const pe of plan) {
865     let z_top = pe.z_top;
866     let z_bot = pe.z_bot;
867     if (! pe.content.length) continue;
868     if (z_bot == null) {
869       let first_z = pe.content[0].p.z;
870       if (z_top >= first_z)
871         z_top = first_z;
872     }
873     let zrange = wasm_bindgen.range(z_bot, z_top, pe.content.length);
874     console.log('LOQER PLAN PE',
875                 pe, z_bot, z_top, pe.content.length, zrange.debug());
876     for (const e of pe.content) {
877       let p = e.p;
878       p.held_us_raising = "Lowered";
879       piece_set_zlevel(e.piece, p, (oldtop_piece) => {
880         let z = zrange.next();
881         p.z = z;
882         api_piece("setz", e.piece, e.p, { z });
883       });
884     }
885   }
886   return null;
887 }
888
889 keyops_local['wrest'] = function (uo: UoRecord) {
890   wresting = !wresting;
891   mousecursor_etc_reupdate();
892 }
893
894 keyops_local['motion-hint-history'] = function (uo: UoRecord) {
895   movehist_len_i ++;
896   movehist_len_i %= movehist_lens.length;
897   movehist_revisible();
898 }
899
900 keyops_local['pin'  ] = function (uo) {
901   if (!lower_targets(uo)) return;
902   pin_unpin(uo, true);
903 }
904 keyops_local['unpin'] = function (uo) {
905   pin_unpin(uo, false);
906 }
907
908 function pin_unpin(uo: UoRecord, newpin: boolean) {
909   for (let piece of uo.targets!) {
910     let p = pieces[piece]!;
911     p.pinned = newpin;
912     api_piece('pin', piece,p, newpin);
913     redisplay_ancillaries(piece,p);
914   }
915   recompute_keybindings();
916 }
917
918 // ----- raising -----
919
920 keyops_local['raise'] = function (uo: UoRecord) { raise_targets(uo); }
921
922 function raise_targets(uo: UoRecord) {
923   let any = false;
924   for (let piece of uo.targets!) {
925     let p = pieces[piece]!;
926     if (p.pinned || !piece_moveable(p)) continue;
927     any = true;
928     piece_raise(piece, p, "NotYet");
929   }
930   if (!any) { 
931     add_log_message('No pieces could be raised.');
932   }
933 }
934
935 function piece_raise(piece: PieceId, p: PieceInfo,
936                      new_held_us_raising: HeldUsRaising,
937   implement: (piece: PieceId, p: PieceInfo, z: ZCoord) => void
938   = function(piece: PieceId, p: PieceInfo, z: ZCoord) {
939     api_piece("setz", piece,p, { z: z });
940   })
941 {
942   p.held_us_raising = new_held_us_raising;
943   piece_set_zlevel(piece,p, (oldtop_piece) => {
944     let oldtop_p = pieces[oldtop_piece]!;
945     let z = wasm_bindgen.increment(oldtop_p.z);
946     p.z = z;
947     implement(piece,p,z);
948   });
949 }
950
951 // ----- clicking/dragging pieces -----
952
953 type DragInfo = {
954   piece : PieceId,
955   dox : number,
956   doy : number,
957 }
958
959 enum DRAGGING { // bitmask
960   NO           = 0x00,
961   MAYBE_GRAB   = 0x01,
962   MAYBE_UNGRAB = 0x02,
963   YES          = 0x04,
964   RAISED       = 0x08,
965 };
966
967 var drag_pieces : DragInfo[] = [];
968 var dragging = DRAGGING.NO;
969 var dcx : number | null;
970 var dcy : number | null;
971
972 const DRAGTHRESH = 5;
973
974 let rectsel_start: Pos | null;
975 let rectsel_shifted: boolean | null;
976 const RECTSELTHRESH = 5;
977
978 function piece_xy(p: PieceInfo): Pos {
979   return [ parseFloat(p.uelem.getAttributeNS(null,"x")!),
980            parseFloat(p.uelem.getAttributeNS(null,"y")!) ];
981 }
982
983 function drag_start_prepare(new_dragging: DRAGGING) {
984   dragging = new_dragging;
985
986   let spos_map = Object.create(null);
987   for (let piece of Object.keys(pieces)) {
988     let p = pieces[piece]!;
989     if (p.held != us) continue;
990     let spos = piece_xy(p);
991     let sposk = `${spos[0]} ${spos[1]}`;
992     if (spos_map[sposk] === undefined) spos_map[sposk] = [spos, []];
993     spos_map[sposk][1].push([spos, piece,p]);
994   }
995
996   for (let sposk of Object.keys(spos_map)) {
997     let [[dox, doy], ents] = spos_map[sposk];
998     for (let i=0; i<ents.length; i++) {
999       let [p, piece] = ents[i];
1000       let delta = (-(ents.length-1)/2 + i) * SPECIAL_MULTI_DELTA_EACH;
1001       p.drag_delta = Math.min(Math.max(delta, -SPECIAL_MULTI_DELTA_MAX),
1002                                               +SPECIAL_MULTI_DELTA_MAX);
1003       drag_pieces.push({
1004         piece: piece,
1005         dox: dox + p.drag_delta,
1006         doy: doy,
1007       });
1008     }
1009   }
1010 }
1011
1012 function some_mousedown(e : MouseEvent) {
1013   console.log('mousedown', e, e.clientX, e.clientY, e.target);
1014
1015   if (e.button != 0) { return }
1016   if (e.altKey) { return }
1017   if (e.metaKey) { return }
1018   if (e.ctrlKey) {
1019     return;
1020   } else {
1021     drag_mousedown(e, e.shiftKey);
1022   }
1023 }
1024
1025 type MouseFindClicked = null | {
1026   clicked: PieceId[],
1027   held: PlayerId | null,
1028   pinned: boolean,
1029   multigrab?: number,
1030 };
1031
1032 type PieceSet = { [piece: string]: true };
1033
1034 function grab_clicked(clicked: PieceId[], loose: boolean,
1035                       multigrab: number | undefined) {
1036   for (let piece of clicked) {
1037     let p = pieces[piece]!;
1038     set_grab_us(piece,p);
1039     if (multigrab === undefined) {
1040       api_piece_x(api_immediate, loose,
1041                   wresting ? 'wrest' : 'grab', piece,p, { });
1042     } else {
1043       piece_raise(piece,p, 'Raised', function(piece,p,z) {
1044         api_piece_x(api_immediate, loose, 'multigrab',
1045                     piece,p, { n: multigrab, z: z });
1046       })
1047     }
1048   }
1049 }
1050 function ungrab_clicked(clicked: PieceId[]) {
1051   let todo: [PieceId, PieceInfo][] = [];
1052   for (let tpiece of clicked) {
1053     let tp = pieces[tpiece]!;
1054     todo.push([tpiece, tp]);
1055   }
1056   do_ungrab_n(todo);
1057 }
1058
1059 function mouse_clicked_one(piece: PieceId, p: PieceInfo): MouseFindClicked {
1060   let held = p.held;
1061   let pinned = p.pinned;
1062   return { clicked: [piece], held, pinned };
1063 }
1064
1065 function mouse_find_predicate(
1066   wanted: number | null,
1067   allow_for_deselect: boolean,
1068   note_already: PieceSet | null,
1069   predicate: (p: PieceInfo) => boolean
1070 ): MouseFindClicked {
1071   let clicked: PieceId[];
1072   let held: string | null;
1073   let pinned = false;
1074   let already_count = 0;
1075
1076   clicked = [];
1077   let uelem = defs_marker;
1078   while (wanted == null || (clicked.length + already_count) < wanted) {
1079     let i = clicked.length;
1080     uelem = uelem.previousElementSibling as any;
1081     if (uelem == pieces_marker) {
1082       if (wanted != null) {
1083         add_log_message(`Not enough pieces!  Stopped after ${i}.`);
1084         return null;
1085       }
1086       break;
1087     }
1088     let piece = uelem.dataset.piece!;
1089
1090     function is_already() {
1091       if (note_already != null) {
1092         already_count++;
1093         note_already[piece] = true;
1094       }
1095     }
1096
1097     let p = pieces[piece];
1098     if (p.pinned && !wresting) continue;
1099     if (p.held && p.held != us && !wresting) continue;
1100     if (i > 0 && !piece_moveable(p))
1101       continue;
1102     if (!predicate(p)) {
1103       continue;
1104     }
1105     if (p.pinned) pinned = true;
1106
1107     if (i == 0) {
1108       held = p.held;
1109       if (held == us && !allow_for_deselect) held = null;
1110     }
1111     if (held! == us) {
1112       // user is going to be deselecting
1113       if (p.held != us) {
1114         // skip ones we don't have
1115         is_already();
1116         continue;
1117       }
1118     } else { // user is going to be selecting
1119       if (p.held == us) {
1120         is_already();
1121         continue; // skip ones we have already
1122       } else if (p.held == null) {
1123       } else {
1124         held = p.held; // wrestish
1125       }
1126     }
1127     clicked.push(piece);
1128   }
1129   if (clicked.length == 0) return null;
1130   else return { clicked, held: held!, pinned: pinned! };
1131 }
1132
1133 function mouse_find_lowest(e: MouseEvent) {
1134   let clickpos = mouseevent_pos(e);
1135   let uelem = pieces_marker;
1136   for (;;) {
1137     uelem = uelem.nextElementSibling as any;
1138     if (uelem == defs_marker) break;
1139     let piece = uelem.dataset.piece!;
1140     let p = pieces[piece]!;
1141     if (p_bbox_contains(p, clickpos)) {
1142       return mouse_clicked_one(piece, p);
1143     }
1144   }
1145   return null;
1146 }
1147
1148 function mouse_find_clicked(e: MouseEvent,
1149                             target: SVGGraphicsElement, piece: PieceId,
1150                             count_allow_for_deselect: boolean,
1151                             note_already: PieceSet | null,
1152                             ): MouseFindClicked
1153 {
1154   let p = pieces[piece]!;
1155   if (special_count == null) {
1156     return mouse_clicked_one(piece, p);
1157   } else if (special_count == 0) {
1158     return mouse_find_lowest(e);
1159   } else { // special_count > 0
1160     if (p.multigrab && !wresting) {
1161       let clicked = mouse_clicked_one(piece, p);
1162       if (clicked) clicked.multigrab = special_count;
1163       return clicked;
1164     } else {
1165       if (special_count > 99) {
1166         add_log_message(
1167           `Refusing to try to select ${special_count} pieces (max is 99)`);
1168         return null;
1169       }
1170       let clickpos = mouseevent_pos(e);
1171       return mouse_find_predicate(
1172         special_count, count_allow_for_deselect, note_already,
1173         function(p) { return p_bbox_contains(p, clickpos); }
1174       )
1175     }
1176   }
1177 }
1178
1179 function drag_mousedown(e : MouseEvent, shifted: boolean) {
1180   let target = e.target as SVGGraphicsElement; // we check this just now!
1181   let piece: PieceId | undefined = target.dataset.piece;
1182
1183   if (!piece) {
1184     rectsel_start = mouseevent_pos(e);
1185     rectsel_shifted = shifted;
1186     window.addEventListener('mousemove', rectsel_mousemove, true);
1187     window.addEventListener('mouseup',   rectsel_mouseup,   true);
1188     return;
1189   }
1190
1191   let note_already = shifted ? null : Object.create(null);
1192
1193   let c = mouse_find_clicked(e, target, piece, false, note_already);
1194   if (c == null) return;
1195   let clicked = c.clicked;
1196   let held = c.held;
1197   let pinned = c.pinned;
1198   let multigrab = c.multigrab;
1199
1200   special_count = null;
1201   mousecursor_etc_reupdate();
1202   drag_cancel();
1203
1204   drag_pieces = [];
1205   if (held == us) {
1206     if (shifted) {
1207       ungrab_clicked(clicked);
1208       return;
1209     }
1210     drag_start_prepare(DRAGGING.MAYBE_UNGRAB);
1211   } else if (held == null || wresting) {
1212     if (!shifted) {
1213       ungrab_all_except(note_already);
1214     }
1215     if (pinned && !wresting) {
1216       let p = pieces[c.clicked[0]!]!;
1217       add_log_message('That piece ('+p.desc+') is pinned to the table.');
1218       return;
1219     }
1220     grab_clicked(clicked, !wresting, multigrab);
1221     drag_start_prepare(DRAGGING.MAYBE_GRAB);
1222   } else {
1223     add_log_message('That piece is held by another player.');
1224     return;
1225   }
1226   dcx = e.clientX;
1227   dcy = e.clientY;
1228
1229   window.addEventListener('mousemove', drag_mousemove, true);
1230   window.addEventListener('mouseup',   drag_mouseup,   true);
1231 }
1232
1233 function mouseevent_pos(e: MouseEvent): Pos {
1234   let ctm = space.getScreenCTM()!;
1235   let px = (e.clientX - ctm.e)/(ctm.a * firefox_bug_zoom_factor_compensation);
1236   let py = (e.clientY - ctm.f)/(ctm.d * firefox_bug_zoom_factor_compensation);
1237   let pos: Pos = [px, py];
1238   console.log('mouseevent_pos', pos);
1239   return pos;
1240 }
1241
1242 function p_bbox_contains(p: PieceInfo, test: Pos) {
1243   let ctr = piece_xy(p);
1244   for (let i of [0,1]) {
1245     let offset = test[i] - ctr[i];
1246     if (offset < p.bbox[0][i] || offset > p.bbox[1][i])
1247       return false;
1248   }
1249   return true;
1250 }
1251
1252 function do_ungrab_n(todo: [PieceId, PieceInfo][]) {
1253   function sort_with(a: [PieceId, PieceInfo],
1254                      b: [PieceId, PieceInfo]): number {
1255     return piece_z_cmp(a[1], b[1]);
1256   }
1257   todo.sort(sort_with);
1258   for (let [tpiece, tp] of todo) {
1259     do_ungrab_1(tpiece, tp);
1260   }
1261 }
1262 function ungrab_all_except(dont: PieceSet | null) {
1263   let todo: [PieceId, PieceInfo][] =  [];
1264   for (let tpiece of Object.keys(pieces)) {
1265     if (dont && dont[tpiece]) continue;
1266     let tp = pieces[tpiece]!;
1267     if (tp.held == us) {
1268       todo.push([tpiece, tp]);
1269     }
1270   }
1271   do_ungrab_n(todo);
1272 }
1273 function ungrab_all() {
1274   ungrab_all_except(null);
1275 }
1276
1277 function set_grab_us(piece: PieceId, p: PieceInfo) {
1278   p.held = us;
1279   p.held_us_raising = "NotYet";
1280   p.drag_delta = 0;
1281   redisplay_ancillaries(piece,p);
1282   recompute_keybindings();
1283 }
1284 function do_ungrab_1(piece: PieceId, p: PieceInfo) {
1285   let autoraise = p.held_us_raising == "Raised";
1286   p.held = null;
1287   p.held_us_raising = "NotYet";
1288   p.drag_delta = 0;
1289   redisplay_ancillaries(piece,p);
1290   recompute_keybindings();
1291   api_piece('ungrab', piece,p, { autoraise });
1292 }
1293
1294 function clear_halo(piece: PieceId, p: PieceInfo) {
1295   let was = p.last_seen_moved;
1296   p.last_seen_moved = null;
1297   if (was) redisplay_ancillaries(piece,p);
1298 }
1299
1300 function ancillary_node(piece: PieceId, stroke: string): SVGGraphicsElement {
1301   var nelem = document.createElementNS(svg_ns,'use');
1302   nelem.setAttributeNS(null,'href','#surround'+piece);
1303   nelem.setAttributeNS(null,'stroke',stroke);
1304   nelem.setAttributeNS(null,'fill','none');
1305   return nelem as any;
1306 }
1307
1308 function redisplay_ancillaries(piece: PieceId, p: PieceInfo) {
1309   let href = '#surround'+piece;
1310   console.log('REDISPLAY ANCILLARIES',href);
1311
1312   for (let celem = p.pelem.firstElementChild;
1313        celem != null;
1314        celem = nextelem) {
1315     var nextelem = celem.nextElementSibling
1316     let thref = celem.getAttributeNS(null,"href");
1317     if (thref == href) {
1318       celem.remove();
1319     }
1320   }
1321
1322   let halo_colour = null;
1323   if (p.cseq_updatesvg != null) {
1324     halo_colour = 'purple';
1325   } else if (p.last_seen_moved != null) {
1326     halo_colour = 'yellow';
1327   } else if (p.held != null && p.pinned) {
1328     halo_colour = '#8cf';
1329   }
1330   if (halo_colour != null) {
1331     let nelem = ancillary_node(piece, halo_colour);
1332     if (p.held != null) {
1333       // value 2ps is also in src/pieces.rs SELECT_STROKE_WIDTH
1334       nelem.setAttributeNS(null,'stroke-width','2px');
1335     }
1336     p.pelem.prepend(nelem);
1337   } 
1338   if (p.held != null) {
1339     let da = null;
1340     if (p.held != us) {
1341       da = players[p.held!]!.dasharray;
1342     } else {
1343       let [px, py] = piece_xy(p);
1344       let inoccult = occregions.contains_pos(px, py);
1345       p.held_us_inoccult = inoccult;
1346       if (inoccult) {
1347         da = "0.9 0.6"; // dotted dasharray
1348       }
1349     }
1350     let nelem = ancillary_node(piece, held_surround_colour);
1351     if (da !== null) {
1352       nelem.setAttributeNS(null,'stroke-dasharray',da);
1353     }
1354     p.pelem.appendChild(nelem);
1355   }
1356 }
1357
1358 function drag_mousemove(e: MouseEvent) {
1359   var ctm = space.getScreenCTM()!;
1360   var ddx = (e.clientX - dcx!)/(ctm.a * firefox_bug_zoom_factor_compensation);
1361   var ddy = (e.clientY - dcy!)/(ctm.d * firefox_bug_zoom_factor_compensation);
1362   var ddr2 = ddx*ddx + ddy*ddy;
1363   if (!(dragging & DRAGGING.YES)) {
1364     if (ddr2 > DRAGTHRESH) {
1365       for (let dp of drag_pieces) {
1366         let tpiece = dp.piece;
1367         let tp = pieces[tpiece]!;
1368         if (tp.moveable == "Yes") {
1369           continue;
1370         } else if (tp.moveable == "IfWresting") {
1371           if (wresting) continue;
1372           add_log_message('That piece can only be moved when Wresting.');
1373         } else {
1374           add_log_message('That piece cannot be moved at the moment.');
1375         }
1376         return ddr2;
1377       }
1378       dragging |= DRAGGING.YES;
1379     }
1380   }
1381   //console.log('mousemove', ddx, ddy, dragging);
1382   if (dragging & DRAGGING.YES) {
1383     console.log('DRAG PIECES',drag_pieces);
1384     for (let dp of drag_pieces) {
1385       console.log('DRAG PIECES PIECE',dp);
1386       let tpiece = dp.piece;
1387       let tp = pieces[tpiece]!;
1388       var x = Math.round(dp.dox + ddx);
1389       var y = Math.round(dp.doy + ddy);
1390       let need_redisplay_ancillaries = (
1391         tp.held == us &&
1392         occregions.contains_pos(x,y) != tp.held_us_inoccult
1393       );
1394       piece_set_pos_core(tp, x, y);
1395       tp.queued_moves++;
1396       api_piece_x(api_delay, false, 'm', tpiece,tp, [x, y] );
1397       if (need_redisplay_ancillaries) redisplay_ancillaries(tpiece, tp);
1398     }
1399     if (!(dragging & DRAGGING.RAISED)) {
1400       sort_drag_pieces();
1401       for (let dp of drag_pieces) {
1402         let piece = dp.piece;
1403         let p = pieces[piece]!;
1404         if (p.held_us_raising == "Lowered") continue;
1405         let dragraise = +p.pelem.dataset.dragraise!;
1406         if (dragraise > 0 && ddr2 >= dragraise*dragraise) {
1407           dragging |= DRAGGING.RAISED;
1408           console.log('CHECK RAISE ', dragraise, dragraise*dragraise, ddr2);
1409           piece_raise(piece,p,"Raised");
1410         }
1411       }
1412     }
1413   }
1414   return ddr2;
1415 }
1416 function sort_drag_pieces() {
1417   function sort_with(a: DragInfo, b: DragInfo): number {
1418     return pieceid_z_cmp(a.piece,
1419                          b.piece);
1420   }
1421   drag_pieces.sort(sort_with);
1422 }
1423
1424 function drag_mouseup(e: MouseEvent) {
1425   console.log('mouseup', dragging);
1426   let ddr2 : number = drag_mousemove(e);
1427   drag_end();
1428 }
1429
1430 function drag_end() {
1431   if (dragging == DRAGGING.MAYBE_UNGRAB ||
1432       (dragging & ~DRAGGING.RAISED) == (DRAGGING.MAYBE_GRAB | DRAGGING.YES)) {
1433     sort_drag_pieces();
1434     for (let dp of drag_pieces) {
1435       let piece = dp.piece;
1436       let p = pieces[piece]!;
1437       do_ungrab_1(piece,p);
1438     }
1439   }
1440   drag_cancel();
1441 }
1442
1443 function drag_cancel() {
1444   window.removeEventListener('mousemove', drag_mousemove, true);
1445   window.removeEventListener('mouseup',   drag_mouseup,   true);
1446   dragging = DRAGGING.NO;
1447   drag_pieces = [];
1448 }
1449
1450 function rectsel_nontrivial_pos2(e: MouseEvent): Pos | null {
1451   let pos2 = mouseevent_pos(e);
1452   let d2 = 0;
1453   for (let i of [0,1]) {
1454     let d = pos2[i] - rectsel_start![i];
1455     d2 += d*d;
1456   }
1457   return d2 > RECTSELTHRESH*RECTSELTHRESH ? pos2 : null;
1458 }
1459
1460 function rectsel_mousemove(e: MouseEvent) {
1461   let pos2 = rectsel_nontrivial_pos2(e);
1462   let path;
1463   if (pos2 == null) {
1464     path = "";
1465   } else {
1466     let pos1 = rectsel_start!;
1467     path = `M ${ pos1 [0]} ${ pos1 [1] }
1468               ${ pos2 [0]} ${ pos1 [1] }
1469             M ${ pos1 [0]} ${ pos2 [1] }
1470               ${ pos2 [0]} ${ pos2 [1] }
1471             M ${ pos1 [0]} ${ pos1 [1] }
1472               ${ pos1 [0]} ${ pos2 [1] }
1473             M ${ pos2 [0]} ${ pos1 [1] }
1474               ${ pos2 [0]} ${ pos2 [1] }`;
1475   }
1476   rectsel_path.firstElementChild!.setAttributeNS(null,'d',path);
1477 }
1478
1479 function rectsel_mouseup(e: MouseEvent) {
1480   console.log('rectsel mouseup');
1481   window.removeEventListener('mousemove', rectsel_mousemove, true);
1482   window.removeEventListener('mouseup',   rectsel_mouseup,   true);
1483   rectsel_path.firstElementChild!.setAttributeNS(null,'d','');
1484   let pos2 = rectsel_nontrivial_pos2(e);
1485
1486   if (pos2 == null) {
1487     // clicked not on a piece, and didn't drag
1488     special_count = null;
1489     mousecursor_etc_reupdate();
1490     // we'll bail in a moment, after possibly unselecting things
1491   }
1492
1493   let note_already = Object.create(null);
1494   let c = null;
1495
1496   if (pos2 != null) {
1497     if (special_count != null && special_count == 0) {
1498       add_log_message(`Cannot drag-select lowest.`);
1499       return;
1500     }
1501     let tl = [0,0];
1502     let br = [0,0];
1503     for (let i of [0,1]) {
1504       tl[i] = Math.min(rectsel_start![i], pos2[i]);
1505       br[i] = Math.max(rectsel_start![i], pos2[i]);
1506     }
1507     c = mouse_find_predicate(
1508       special_count, rectsel_shifted!, note_already,
1509       function(p: PieceInfo) {
1510         let pp = piece_xy(p);
1511         for (let i of [0,1]) {
1512           if (pp[i] < tl[i] || pp[i] > br[i]) return false;
1513         }
1514         return true;
1515       }
1516     );
1517   }
1518
1519   if (!c) {
1520     // clicked not on a piece, didn't end up selecting anything
1521     // either because drag region had nothing in it, or special
1522     // failed, or some such.
1523     if (!rectsel_shifted) {
1524       let mr;
1525       while (mr = movements.pop()) {
1526         mr.p.last_seen_moved = null;
1527         redisplay_ancillaries(mr.piece, mr.p);
1528       }
1529       ungrab_all();
1530     }
1531     return;
1532   }
1533
1534   // did the special
1535   special_count = null;
1536   mousecursor_etc_reupdate();
1537
1538   if (rectsel_shifted && c.held == us) {
1539     ungrab_clicked(c.clicked);
1540     return;
1541   } else {
1542     if (!rectsel_shifted) {
1543       ungrab_all_except(note_already);
1544     }
1545     grab_clicked(c.clicked, false, undefined);
1546   }
1547 }
1548
1549 // ----- general -----
1550
1551 type PlayersUpdate = { new_info_pane: string };
1552
1553 messages.SetPlayer = <MessageHandler>function
1554 (j: { player: string, data: PlayerInfo } & PlayersUpdate) {
1555   players[j.player] = j.data;
1556   player_info_pane_set(j);
1557 }
1558
1559 messages.RemovePlayer = <MessageHandler>function
1560 (j: { player: string } & PlayersUpdate ) {
1561   delete players[j.player];
1562   player_info_pane_set(j);
1563 }
1564
1565 function player_info_pane_set(j: PlayersUpdate) {
1566   document.getElementById('player_list')!
1567     .innerHTML = j.new_info_pane;
1568 }
1569
1570 messages.UpdateBundles = <MessageHandler>function
1571 (j: { new_info_pane: string }) {
1572   document.getElementById('bundle_list')!
1573     .innerHTML = j.new_info_pane;
1574 }
1575
1576 messages.SetTableSize = <MessageHandler>function
1577 ([x, y]: [number, number]) {
1578   function set_attrs(elem: Element, l: [string,string][]) {
1579     for (let a of l) {
1580       elem.setAttributeNS(null,a[0],a[1]);
1581     }
1582   }
1583   let rect = document.getElementById('table_rect')!;
1584   set_attrs(space, wasm_bindgen.space_table_attrs(x, y));
1585   set_attrs(rect,  wasm_bindgen.space_table_attrs(x, y));
1586 }
1587
1588 messages.SetTableColour = <MessageHandler>function
1589 (c: string) {
1590   let rect = document.getElementById('table_rect')!;
1591   rect.setAttributeNS(null, 'fill', c);
1592 }
1593
1594 messages.SetLinks = <MessageHandler>function
1595 (msg: string) {
1596   if (msg.length != 0 && layout == 'Portrait') {
1597     msg += " |";
1598   }
1599   links_elem.innerHTML = msg
1600 }
1601
1602 // ---------- movehist ----------
1603
1604 type MoveHistEnt = {
1605   held: PlayerId,
1606   posx: [MoveHistPosx, MoveHistPosx],
1607   diff: { 'Moved': { d: number } },
1608 }
1609 type MoveHistPosx = {
1610   pos: Pos,
1611   angle: CompassAngle,
1612   facehint: FaceId | null,
1613 }
1614
1615 messages.MoveHistEnt = <MessageHandler>movehist_record;
1616 messages.MoveHistClear = <MessageHandler>function() {
1617   movehist_revisible_custmax(0);
1618 }
1619
1620 function movehist_record(ent: MoveHistEnt) {
1621   let old_pos = ent.posx[0].pos;
1622   let new_pos = ent.posx[1].pos;
1623
1624   movehist_gen++;
1625   movehist_gen %= (movehist_len_max * 2);
1626   let meid = 'motionhint-marker-' + movehist_gen;
1627
1628   let moved = ent.diff['Moved'];
1629   if (moved) {
1630     let d = moved.d;
1631     let ends = [];
1632     for (let end of [0,1]) {
1633       let s = (!end ? MOVEHIST_ENDS : d - MOVEHIST_ENDS) / d;
1634       ends.push([ (1-s) * old_pos[0] + s * new_pos[0],
1635                   (1-s) * old_pos[1] + s * new_pos[1] ]);
1636     }
1637     let g = document.createElementNS(svg_ns,'g');
1638     let sz = 4;
1639     let pi = players[ent.held];
1640     let nick = pi ? pi.nick : '';
1641     // todo: would be nice to place text variously along arrow, rotated
1642     let svg = `
1643       <marker id="${meid}" viewBox="2 0 ${sz} ${sz}" 
1644         refX="${sz}" refY="${sz/2}"
1645         markerWidth="${sz + 2}" markerHeight="${sz}"
1646         stroke="yellow" fill="none"
1647         orient="auto-start-reverse" stroke-linejoin="miter">
1648         <path d="M 0 0 L ${sz} ${sz/2} L 0 ${sz}" />
1649       </marker>
1650       <line x1="${ends[0][0].toString()}"
1651             y1="${ends[0][1].toString()}"
1652             x2="${ends[1][0].toString()}"
1653             y2="${ends[1][1].toString()}"
1654             stroke="yellow"
1655             stroke-width="1" pointer-events="none"
1656             marker-end="url(#${meid})" />
1657       <text x="${((ends[0][0] + ends[1][0]) / 2).toString()}"
1658             y="${((ends[0][1] + ends[1][1]) / 2).toString()}"
1659             font-size="5" pointer-events="none"
1660             stroke-width="0.1">${nick}</text>
1661     `;
1662     g.innerHTML = svg;
1663     space.insertBefore(g, movehist_end);
1664     movehist_revisible();
1665   }
1666 }
1667
1668 function movehist_revisible() { 
1669   movehist_revisible_custmax(movehist_len_max);
1670 }
1671
1672 function movehist_revisible_custmax(len_max: number) {
1673   let n = movehist_lens[movehist_len_i];
1674   let i = 0;
1675   let node = movehist_end;
1676   while (i < len_max) {
1677     i++; // i now eg 1..10
1678     node = node.previousElementSibling! as SVGGraphicsElement;
1679     if (node == movehist_start)
1680       return;
1681     let prop = i > n ? 0 : (n-i+1)/n;
1682     let stroke = (prop * 1.0).toString();
1683     let marker = node.firstElementChild!;
1684     marker.setAttributeNS(null,'stroke-width',stroke);
1685     let line = marker.nextElementSibling!;
1686     line.setAttributeNS(null,'stroke-width',stroke);
1687     let text = line.nextElementSibling!;
1688     if (!prop) {
1689       text.setAttributeNS(null,'stroke','none');
1690       text.setAttributeNS(null,'fill','none');
1691     } else {
1692       text.setAttributeNS(null,'fill','yellow');
1693       text.setAttributeNS(null,'stroke','orange');
1694     }
1695   }
1696   for (;;) {
1697     let del = node.previousElementSibling!;
1698     if (del == movehist_start)
1699       return;
1700     del.remove();
1701   }
1702 }  
1703
1704 // ----- logs -----
1705
1706 messages.Log = <MessageHandler>function
1707 (j: { when: string, logent: { html: string } }) {
1708   add_timestamped_log_message(j.when, j.logent.html);
1709 }
1710
1711 function add_log_message(msg_html: string) {
1712   add_timestamped_log_message('', msg_html);
1713 }
1714
1715 function add_timestamped_log_message(ts_html: string, msg_html: string) {
1716   var lastent = log_elem.lastElementChild;
1717   var in_scrollback =
1718     lastent == null ||
1719     // inspired by
1720     //   https://stackoverflow.com/questions/487073/how-to-check-if-element-is-visible-after-scrolling/21627295#21627295
1721     // rejected
1722       //   https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API
1723       (() => {
1724         let le_top = lastent.getBoundingClientRect()!.top;
1725         let le_bot = lastent.getBoundingClientRect()!.bottom;
1726         let ld_bot = logscroll_elem.getBoundingClientRect()!.bottom;
1727         console.log("ADD_LOG_MESSAGE bboxes: le t b, bb",
1728                     le_top, le_bot, ld_bot);
1729         return 0.5 * (le_bot + le_top) > ld_bot;
1730       })();
1731
1732   console.log('ADD LOG MESSAGE ',in_scrollback, layout, msg_html);
1733
1734   var ne : HTMLElement;
1735
1736   function add_thing(elemname: string, cl: string, html: string) {
1737     var ie = document.createElement(elemname);
1738     ie.innerHTML = html;
1739     ie.setAttribute("class", cl);
1740     ne.appendChild(ie);
1741   }
1742
1743   if (layout == 'Portrait') {
1744     ne = document.createElement('tr');
1745     add_thing('td', 'logmsg', msg_html);
1746     add_thing('td', 'logts',  ts_html);
1747   } else if (layout == 'Landscape') {
1748     ts_html = last_log_ts.update(ts_html);
1749     ne = document.createElement('div');
1750     add_thing('span', 'logts',  ts_html);
1751     ne.appendChild(document.createElement('br'));
1752     add_thing('span', 'logmsg', msg_html);
1753     ne.appendChild(document.createElement('br'));
1754   } else {
1755     throw 'bad layout ' + layout;
1756   }
1757   log_elem.appendChild(ne);
1758
1759   if (!in_scrollback) {
1760     logscroll_elem.scrollTop = logscroll_elem.scrollHeight;
1761   }
1762 }
1763
1764 // ----- zoom -----
1765
1766 function zoom_pct (): number | undefined {
1767   let str = zoom_val.value;
1768   let val = parseFloat(str);
1769   if (isNaN(val)) {
1770     return undefined;
1771   } else {
1772     return val;
1773   }
1774 }
1775
1776 function zoom_enable() {
1777   zoom_btn.disabled = (zoom_pct() === undefined);
1778 }
1779
1780 function zoom_activate() {
1781   let pct = zoom_pct();
1782   if (pct !== undefined) {
1783     let fact = pct * 0.01;
1784     let last_ctm_a = space.getScreenCTM()!.a;
1785     (document.getElementsByTagName('body')[0] as HTMLElement)
1786       .style.transform = 'scale('+fact+','+fact+')';
1787     if (fact != last_zoom_factor) {
1788       if (last_ctm_a == space.getScreenCTM()!.a) {
1789         console.log('FIREFOX GETSCREENCTM BUG');
1790         firefox_bug_zoom_factor_compensation = fact;
1791       } else {
1792         console.log('No firefox getscreenctm bug');
1793         firefox_bug_zoom_factor_compensation = 1.0;
1794       }
1795       last_zoom_factor = fact;
1796     }
1797   }
1798   zoom_btn.disabled = true;
1799 }
1800
1801 // ----- test counter, startup -----
1802
1803 type TransmitUpdateEntry_Piece = {
1804   piece: PieceId,
1805   op: Object,
1806 };
1807 type ErrorTransmitUpdateEntry_Piece = TransmitUpdateEntry_Piece & {
1808   cseq: ClientSeq | null,
1809 };
1810
1811 function handle_piece_update(j: TransmitUpdateEntry_Piece) {
1812   console.log('PIECE UPDATE ',j)
1813   var piece = j.piece;
1814   var m = j.op as { [k: string]: Object };
1815   var k = Object.keys(m)[0];
1816   let p = pieces[piece];
1817   pieceops[k](piece,p, m[k]);
1818 };
1819
1820 messages.Piece = <MessageHandler>handle_piece_update;
1821
1822 type PreparedPieceState = {
1823   pos: Pos,
1824   svg: string,
1825   desc: string,
1826   held: PlayerId | null,
1827   z: ZCoord,
1828   zg: Generation,
1829   pinned: boolean,
1830   angle: number,
1831   uos: UoDescription[],
1832   moveable: PieceMoveable,
1833   rotateable: boolean,
1834   multigrab: boolean,
1835   occregion: string | null,
1836   bbox: Rect,
1837 }
1838
1839 pieceops.ModifyQuiet = <PieceHandler>function
1840 (piece: PieceId, p: PieceInfo, info: PreparedPieceState) {
1841   console.log('PIECE UPDATE MODIFY QUIET ',piece,info)
1842   piece_modify(piece, p, info);
1843 }
1844
1845 pieceops.Modify = <PieceHandler>function
1846 (piece: PieceId, p: PieceInfo, info: PreparedPieceState) {
1847   console.log('PIECE UPDATE MODIFY LOuD ',piece,info)
1848   piece_note_moved(piece,p);
1849   piece_modify(piece, p, info);
1850 }
1851
1852 pieceops.InsertQuiet = <PieceHandler>(insert_piece as any);
1853 pieceops.Insert = <PieceHandler>function
1854 (piece: PieceId, xp: any, info: PreparedPieceState) {
1855   let p = insert_piece(piece,xp,info);
1856   piece_note_moved(piece,p);
1857 }
1858
1859 function insert_piece(piece: PieceId, xp: any,
1860                       info: PreparedPieceState): PieceInfo
1861 {
1862   console.log('PIECE UPDATE INSERT ',piece,info)
1863   let delem = document.createElementNS(svg_ns,'defs');
1864   delem.setAttributeNS(null,'id','defs'+piece);
1865   delem.innerHTML = info.svg;
1866   defs_marker.insertAdjacentElement('afterend', delem);
1867   let pelem = piece_element('piece',piece);
1868   let uelem = document.createElementNS(svg_ns,'use');
1869   uelem.setAttributeNS(null,'id',"use"+piece);
1870   uelem.setAttributeNS(null,'href',"#piece"+piece);
1871   uelem.setAttributeNS(null,'data-piece',piece);
1872   let p = {
1873     uelem: uelem,
1874     pelem: pelem,
1875     delem: delem,
1876   } as any as PieceInfo; // fudge this, piece_modify_core will fix it
1877   pieces[piece] = p;
1878   p.uos = info.uos;
1879   p.queued_moves = 0;
1880   piece_resolve_special(piece, p);
1881   piece_modify_core(piece, p, info);
1882   return p;
1883 }
1884
1885 pieceops.Delete = <PieceHandler>function
1886 (piece: PieceId, p: PieceInfo, info: {}) {
1887   console.log('PIECE UPDATE DELETE ', piece)
1888   piece_stop_special(piece, p);
1889   p.uelem.remove();
1890   p.delem.remove();
1891   delete pieces[piece];
1892   if (p.held == us) {
1893     recompute_keybindings();
1894   }
1895 }
1896
1897 piece_error_handlers.PosOffTable = <PieceErrorHandler>function()
1898 { return true ; }
1899 piece_error_handlers.Conflict = <PieceErrorHandler>function()
1900 { return true ; }
1901
1902 function piece_modify_image(piece: PieceId, p: PieceInfo,
1903                             info: PreparedPieceImage) {
1904   p.delem.innerHTML = info.svg;
1905   p.pelem= piece_element('piece',piece)!;
1906   p.uos = info.uos;
1907   p.bbox = info.bbox;
1908   p.desc = info.desc;
1909   piece_resolve_special(piece, p);
1910 }
1911
1912 function piece_resolve_special(piece: PieceId, p: PieceInfo) {
1913   let new_special =
1914       p.pelem.dataset.special ?
1915       JSON.parse(p.pelem.dataset.special) : null;
1916   let new_special_kind = new_special ? new_special.kind : '';
1917   let old_special_kind = p  .special ? p  .special.kind : '';
1918
1919   if (new_special_kind != old_special_kind) {
1920     piece_stop_special(piece, p);
1921   }
1922   if (new_special) {
1923     console.log('SPECIAL START', new_special);
1924     new_special.stop = function() { };
1925     p.special = new_special;
1926     special_renderings[new_special_kind](piece, p, new_special);
1927   }
1928 }
1929
1930 function piece_stop_special(piece: PieceId, p: PieceInfo) {
1931   let s = p.special;
1932   p.special = null;
1933   if (s) {
1934     console.log('SPECIAL STOP', s);
1935     s.stop(piece, p, s);
1936   }
1937 }
1938
1939 function piece_modify(piece: PieceId, p: PieceInfo, info: PreparedPieceState) {
1940   piece_modify_image(piece, p, info);
1941   piece_modify_core(piece, p, info);
1942 }
1943                        
1944 function piece_set_pos_core(p: PieceInfo, x: number, y: number) {
1945   p.uelem.setAttributeNS(null, "x", x+"");
1946   p.uelem.setAttributeNS(null, "y", y+"");
1947 }
1948
1949 function piece_modify_core(piece: PieceId, p: PieceInfo,
1950                            info: PreparedPieceState) {
1951   p.uelem.setAttributeNS(null, "x", info.pos[0]+"");
1952   p.uelem.setAttributeNS(null, "y", info.pos[1]+"");
1953   p.held = info.held;
1954   p.held_us_raising = "NotYet";
1955   p.pinned = info.pinned;
1956   p.moveable = info.moveable;
1957   p.rotateable = info.rotateable;
1958   p.multigrab = info.multigrab;
1959   p.angle = info.angle;
1960   p.bbox = info.bbox;
1961   piece_set_zlevel_from(piece,p,info);
1962   let occregions_changed = occregion_update(piece, p, info);
1963   piece_checkconflict_nrda(piece,p);
1964   redisplay_ancillaries(piece,p);
1965   if (occregions_changed) redisplay_held_ancillaries();
1966   recompute_keybindings();
1967   console.log('MODIFY DONE');
1968 }
1969 function occregion_update(piece: PieceId, p: PieceInfo,
1970                           info: PreparedPieceState) {
1971   let occregions_changed = (
1972     info.occregion != null
1973       ? occregions.insert(piece, info.occregion)
1974       : occregions.remove(piece)
1975   );
1976   return occregions_changed;
1977 }
1978 function redisplay_held_ancillaries() {
1979   for (let piece of Object.keys(pieces)) {
1980     let p = pieces[piece];
1981     if (p.held != us) continue;
1982     redisplay_ancillaries(piece,p);
1983   }
1984 }
1985
1986 type PreparedPieceImage = {
1987   svg: string,
1988   desc: string,
1989   uos: UoDescription[],
1990   bbox: Rect,
1991 }
1992
1993 type TransmitUpdateEntry_Image = {
1994   piece: PieceId,
1995   im: PreparedPieceImage,
1996 };
1997
1998 messages.Image = <MessageHandler>function(j: TransmitUpdateEntry_Image) {
1999   console.log('IMAGE UPDATE ',j)
2000   var piece = j.piece;
2001   let p = pieces[piece]!;
2002   piece_modify_image(piece, p, j.im);
2003   redisplay_ancillaries(piece,p);
2004   recompute_keybindings();
2005   console.log('IMAGE DONE');
2006 }
2007
2008 function piece_set_zlevel(piece: PieceId, p: PieceInfo,
2009                           modify : (oldtop_piece: PieceId) => void) {
2010   // Calls modify, which should set .z and/or .gz, and/or
2011   // make any necessary API call.
2012   //
2013   // Then moves uelem to the right place in the DOM.  This is done
2014   // by assuming that uelem ought to go at the end, so this is
2015   // O(new depth), which is right (since the UI for inserting
2016   // an object is itself O(new depth) UI operations to prepare.
2017
2018   let oldtop_elem = (defs_marker.previousElementSibling! as
2019                      unknown as SVGGraphicsElement);
2020   let oldtop_piece = oldtop_elem.dataset.piece!;
2021   modify(oldtop_piece);
2022
2023   let ins_before = defs_marker
2024   let earlier_elem;
2025   for (; ; ins_before = earlier_elem) {
2026     earlier_elem = (ins_before.previousElementSibling! as
2027                    unknown as SVGGraphicsElement);
2028     if (earlier_elem == pieces_marker) break;
2029     if (earlier_elem == p.uelem) continue;
2030     let earlier_p = pieces[earlier_elem.dataset.piece!]!;
2031     if (!piece_z_before(p, earlier_p)) break;
2032   }
2033   if (ins_before != p.uelem)
2034     space.insertBefore(p.uelem, ins_before);
2035
2036   check_z_order();
2037 }
2038
2039 function check_z_order() {
2040   if (!otter_debug) return;
2041   let s = pieces_marker;
2042   let last_z = "";
2043   for (;;) {
2044     s = s.nextElementSibling as SVGGraphicsElement;
2045     if (s == defs_marker) break;
2046     let piece = s.dataset.piece!;
2047     let z = pieces[piece].z;
2048     if (z < last_z) {
2049       json_report_error(['Z ORDER INCONSISTENCY!', piece, z, last_z]);
2050     }
2051     last_z = z;
2052   }
2053 }
2054
2055 function piece_note_moved(piece: PieceId, p: PieceInfo) {
2056   let now = performance.now();
2057
2058   let need_redisplay = p.last_seen_moved == null;
2059   p.last_seen_moved = now;
2060   if (need_redisplay) redisplay_ancillaries(piece,p);
2061
2062   let cutoff = now-1000.;
2063   while (movements.length > 0 && movements[0].this_motion < cutoff) {
2064     let mr = movements.shift()!;
2065     if (mr.p.last_seen_moved != null &&
2066         mr.p.last_seen_moved < cutoff) {
2067       mr.p.last_seen_moved = null;
2068       redisplay_ancillaries(mr.piece,mr.p);
2069     }
2070   }
2071
2072   movements.push({ piece: piece, p: p, this_motion: now });
2073 }
2074
2075 function piece_z_cmp(a: PieceInfo, b: PieceInfo) {
2076   if (a.z  < b.z ) return -1;
2077   if (a.z  > b.z ) return +1;
2078   if (a.zg < b.zg) return -1;
2079   if (a.zg > b.zg) return +1;
2080   return 0;
2081 }
2082
2083 function piece_z_before(a: PieceInfo, b: PieceInfo) {
2084   return piece_z_cmp(a,
2085                      b) < 0;
2086 }
2087
2088 function pieceid_z_cmp(a: PieceId, b: PieceId) {
2089   return piece_z_cmp(pieces[a]!,
2090                      pieces[b]!);
2091 }
2092
2093 pieceops.Move = <PieceHandler>function
2094 (piece,p, info: Pos ) {
2095   piece_checkconflict_nrda(piece,p);
2096   piece_note_moved(piece, p);
2097   piece_set_pos_core(p, info[0], info[1]);
2098 }
2099
2100 pieceops.MoveQuiet = <PieceHandler>function
2101 (piece,p, info: Pos ) {
2102   piece_checkconflict_nrda(piece,p);
2103   piece_set_pos_core(p, info[0], info[1]);
2104 }
2105
2106 pieceops.SetZLevel = <PieceHandler>function
2107 (piece,p, info: { z: ZCoord, zg: Generation }) {
2108   piece_note_moved(piece,p);
2109   piece_set_zlevel_from(piece,p,info);
2110 }
2111
2112 pieceops.SetZLevelQuiet = <PieceHandler>function
2113 (piece,p, info: { z: ZCoord, zg: Generation }) {
2114   piece_set_zlevel_from(piece,p,info);
2115 }
2116
2117 function piece_set_zlevel_from(piece: PieceId, p: PieceInfo,
2118                                info: { z: ZCoord, zg: Generation }) {
2119   piece_set_zlevel(piece,p, (oldtop_piece)=>{
2120     p.z  = info.z;
2121     p.zg = info.zg;
2122   });
2123 }
2124
2125 messages.Recorded = <MessageHandler>function
2126 (j: { piece: PieceId, cseq: ClientSeq,
2127       zg: Generation|null, svg: string | null, desc: string | null } ) {
2128   let piece = j.piece;
2129   let p = pieces[piece]!;
2130   piece_recorded_cseq(p, j);
2131   if (p.cseq_updatesvg != null && j.cseq >= p.cseq_updatesvg) {
2132     p.cseq_updatesvg = null;
2133     redisplay_ancillaries(piece,p);
2134   }
2135   if (j.svg != null) {
2136     p.delem.innerHTML = j.svg;
2137     p.pelem= piece_element('piece',piece)!;
2138     piece_resolve_special(piece, p);
2139     redisplay_ancillaries(piece,p);
2140   }
2141   if (j.zg != null) {
2142     var zg_new = j.zg; // type narrowing doesn't propagate :-/
2143     piece_set_zlevel(piece,p, (oldtop_piece: PieceId)=>{
2144       p.zg = zg_new;
2145     });
2146   }
2147   if (j.desc != null) {
2148     p.desc = j.desc;
2149   }
2150 }
2151
2152 function piece_recorded_cseq(p: PieceInfo, j: { cseq: ClientSeq }) {
2153   if (p.cseq_main  != null && j.cseq >= p.cseq_main ) { p.cseq_main  = null; }
2154   if (p.cseq_loose != null && j.cseq >= p.cseq_loose) { p.cseq_loose = null; }
2155 }
2156
2157 messages.RecordedUnpredictable = <MessageHandler>function
2158 (j: { piece: PieceId, cseq: ClientSeq, ns: PreparedPieceState } ) {
2159   let piece = j.piece;
2160   let p = pieces[piece]!;
2161   piece_recorded_cseq(p, j);
2162   piece_modify(piece, p, j.ns);
2163 }
2164
2165 messages.Error = <MessageHandler>function
2166 (m: any) {
2167   console.log('ERROR UPDATE ', m);
2168   var k = Object.keys(m)[0];
2169   update_error_handlers[k](m[k]);
2170 }
2171
2172 type PieceOpError = {
2173   error: string,
2174   error_msg: string,
2175   state: ErrorTransmitUpdateEntry_Piece,
2176 };
2177
2178 update_error_handlers.PieceOpError = <MessageHandler>function
2179 (m: PieceOpError) {
2180   let piece = m.state.piece;
2181   let cseq = m.state.cseq;
2182   let p = pieces[piece];
2183   console.log('ERROR UPDATE PIECE ', piece, cseq, m, m.error_msg, p);
2184   if (p == null) return; // who can say!
2185   if (m.error != 'Conflict') {
2186     // Our gen was high enough we we sent this, that it ought to have
2187     // worked.  Report it as a problem, then.
2188     add_log_message('Problem manipulating piece: ' + m.error_msg);
2189     // Mark aus as having no outstanding requests, and cancel any drag.
2190     piece_checkconflict_nrda(piece, p, true);
2191   }
2192   handle_piece_update(m.state);
2193 }
2194
2195 function piece_checkconflict_nrda(piece: PieceId, p: PieceInfo,
2196                                   already_logged: boolean = false) {
2197   // Our state machine for cseq:
2198   //
2199   // When we send an update (api_piece_x) we always set cseq.  If the
2200   // update is loose we also set cseq_beforeloose.  Whenever we
2201   // clear cseq we clear cseq_beforeloose too.
2202   //
2203   // The result is that if cseq_beforeloose is non-null precisely if
2204   // the last op we sent was loose.
2205   //
2206   // We track separately the last loose, and the last non-loose,
2207   // outstanding API request.  (We discard our idea of the last
2208   // loose request if we follow it with a non-loose one.)
2209   //
2210   // So
2211   //     cseq_main > cseq_loose         one loose request then some non-loose
2212   //     cseq_main, no cseq_loose       just non-loose requests
2213   //     no cseq_main, but cseq_loose   just one loose request
2214   //     neither                        no outstanding requests
2215   //
2216   // If our only outstanding update is loose, we ignore a detected
2217   // conflict.  We expect the server to send us a proper
2218   // (non-Conflict) error later.
2219   if (p.cseq_main != null || p.cseq_loose != null) {
2220     if (drag_pieces.some(function(dp) { return dp.piece == piece; })) {
2221       console.log('drag end due to conflict');
2222       drag_end();
2223     }
2224   }
2225   if (p.cseq_main != null) {
2226     if (!already_logged)
2227       add_log_message('Conflict! - simultaneous update');
2228   }
2229   p.cseq_main = null;
2230   p.cseq_loose = null;
2231 }
2232
2233 function test_swap_stack() {
2234   let old_bot = pieces_marker.nextElementSibling!;
2235   let container = old_bot.parentElement!;
2236   container.insertBefore(old_bot, defs_marker);
2237   window.setTimeout(test_swap_stack, 1000);
2238 }
2239
2240 function startup() {
2241   console.log('STARTUP');
2242   console.log(wasm_bindgen.setup("OK"));
2243
2244   var body = document.getElementById("main-body")!;
2245   zoom_btn = document.getElementById("zoom-btn") as any;
2246   zoom_val = document.getElementById("zoom-val") as any;
2247   links_elem = document.getElementById("links") as any;
2248   ctoken = body.dataset.ctoken!;
2249   us = body.dataset.us!;
2250   gen = +body.dataset.gen!;
2251   let sse_url_prefix = body.dataset.sseUrlPrefix!;
2252   status_node = document.getElementById('status')!;
2253   status_node.innerHTML = 'js-done';
2254   log_elem = document.getElementById("log")!;
2255   logscroll_elem = document.getElementById("logscroll") || log_elem;
2256   let dataload = JSON.parse(body.dataset.load!);
2257   held_surround_colour = dataload.held_surround_colour!;
2258   players = dataload.players!;
2259   delete body.dataset.load;
2260   uos_node = document.getElementById("uos")!;
2261   occregions = wasm_bindgen.empty_region_list();
2262
2263   space = svg_element('space')!;
2264   pieces_marker = svg_element("pieces_marker")!;
2265   defs_marker = svg_element("defs_marker")!;
2266   movehist_start = svg_element('movehist_marker')!;
2267   movehist_end = svg_element('movehist_end')!;
2268   rectsel_path = svg_element('rectsel_path')!;
2269   svg_ns = space.getAttribute('xmlns')!;
2270
2271   for (let uelem = pieces_marker.nextElementSibling! as SVGGraphicsElement;
2272        uelem != defs_marker;
2273        uelem = uelem.nextElementSibling! as SVGGraphicsElement) {
2274     let piece = uelem.dataset.piece!;
2275     let p = JSON.parse(uelem.dataset.info!);
2276     p.uelem = uelem;
2277     p.delem = piece_element('defs',piece);
2278     p.pelem = piece_element('piece',piece);
2279     p.queued_moves = 0;
2280     occregion_update(piece, p, p); delete p.occregion;
2281     delete uelem.dataset.info;
2282     pieces[piece] = p;
2283     piece_resolve_special(piece,p);
2284     redisplay_ancillaries(piece,p);
2285   }
2286
2287   if (test_update_hook == null) test_update_hook = function() { };
2288   test_update_hook();
2289
2290   last_log_ts = wasm_bindgen.timestamp_abbreviator(dataload.last_log_ts);
2291
2292   for (let ent of dataload.movehist.hist) {
2293     movehist_record(ent);
2294   }
2295
2296   var es = new EventSource(
2297     sse_url_prefix + "/_/updates?ctoken="+ctoken+'&gen='+gen
2298   );
2299   es.onmessage = function(event) {
2300     console.log('GOTEVE', event.data);
2301     var k;
2302     var m;
2303     try {
2304       var [tgen, ms] = JSON.parse(event.data);
2305       for (m of ms) {
2306         k = Object.keys(m)[0];
2307         messages[k](m[k]);
2308       }
2309       gen = tgen;
2310       test_update_hook();
2311     } catch (exc) {
2312       var s = exc.toString();
2313       string_report_error('exception handling update '
2314                           + k + ': ' + JSON.stringify(m) + ': ' + s);
2315     }
2316   }
2317   es.addEventListener('commsworking', function(event) {
2318     console.log('GOTDATA', (event as any).data);
2319     status_node.innerHTML = (event as any).data;
2320   });
2321   es.addEventListener('player-gone', function(event) {
2322     console.log('PLAYER-GONE', event);
2323     status_node.innerHTML = (event as any).data;
2324     add_log_message('<strong>You are no longer in the game</strong>');
2325     space.removeEventListener('mousedown', some_mousedown);
2326     document.removeEventListener('keydown', some_keydown);
2327     es.close();
2328   });
2329   es.addEventListener('updates-expired', function(event) {
2330     console.log('UPDATES-EXPIRED', event);
2331     string_report_error('connection to server interrupted too long');
2332   });
2333   es.onerror = function(e) {
2334     let info = {
2335       updates_error : e,
2336       updates_event_source : es,
2337       updates_event_source_ready : es.readyState,
2338       update_oe : (e as any).className,
2339     };
2340     if (es.readyState == 2) {
2341       json_report_error({
2342         reason: "TOTAL SSE FAILURE",
2343         info: info,
2344       })
2345     } else {
2346       console.log('SSE error event', info);
2347     }
2348   }
2349   recompute_keybindings();
2350   space.addEventListener('mousedown', some_mousedown);
2351   space.addEventListener('dragstart', function (e) {
2352     e.preventDefault();
2353     e.stopPropagation();
2354   }, true);
2355   document.addEventListener('keydown',   some_keydown);
2356   check_z_order();
2357 }
2358
2359 type DieSpecialRendering = SpecialRendering & {
2360   cd_path: SVGPathElement,
2361   loaded_ts: DOMHighResTimeStamp,
2362   loaded_remprop: number,
2363   total_ms: number,
2364   radius: number,
2365   anim_id: number | null,
2366 };
2367 special_renderings['Die'] = function(piece: PieceId, p: PieceInfo,
2368                                      s: DieSpecialRendering) {
2369   let cd_path = document.getElementById('def.'+piece+'.die.cd');
2370   if (!cd_path) return;
2371
2372   s.cd_path = cd_path as any as SVGPathElement;
2373   s.loaded_ts = performance.now();
2374   s.loaded_remprop = parseFloat(cd_path.dataset.remprop!)!;
2375   s.total_ms       = parseFloat(cd_path.dataset.total_ms!)!;
2376   s.radius         = parseFloat(cd_path.dataset.radius!)!;
2377
2378   s.stop = die_rendering_stop as any;
2379   die_request_animation(piece, p, s);
2380 } as any;
2381 function die_request_animation(piece: PieceId, p: PieceInfo,
2382                                s: DieSpecialRendering) {
2383   s.anim_id = window.requestAnimationFrame(
2384     function(ts) { die_render_frame(piece, p, s, ts) }
2385   );
2386 }
2387 function die_render_frame(piece: PieceId, p: PieceInfo,
2388                           s: DieSpecialRendering, ts: DOMHighResTimeStamp) {
2389   s.anim_id = null;
2390   let remprop = s.loaded_remprop - (ts - s.loaded_ts) / s.total_ms;
2391   //console.log('DIE RENDER', piece, s, remprop);
2392   if (remprop <= 0) {
2393     console.log('DIE COMPLETE', piece, s, remprop);
2394     let to_remove: Element = s.cd_path;
2395     for (;;) {
2396       let previous = to_remove.previousElementSibling!;
2397       // see dice/overlya-template-extractor
2398       if (to_remove.tagName == 'text') break;
2399       to_remove.remove();
2400       to_remove = previous;
2401     }
2402   } else {
2403     let path_d = wasm_bindgen.die_cooldown_path(s.radius, remprop);
2404     s.cd_path.setAttributeNS(null, "d", path_d);
2405     die_request_animation(piece, p, s);
2406   }
2407 }
2408 function die_rendering_stop(piece: PieceId, p: PieceInfo,
2409                             s: DieSpecialRendering) {
2410   let anim_id = s.anim_id;
2411   if (anim_id == null) return;
2412   s.anim_id = null;
2413   window.cancelAnimationFrame(anim_id);
2414 }    
2415
2416 declare var wasm_input : any;
2417 var wasm_promise : Promise<any>;;
2418
2419 function doload(){
2420   console.log('DOLOAD');
2421   globalinfo_elem = document.getElementById('global-info')!;
2422   layout = globalinfo_elem!.dataset!.layout! as any;
2423   var elem = document.getElementById('loading_token')!;
2424   var ptoken = elem.dataset.ptoken;
2425   xhr_post_then('/_/session/' + layout, 
2426                 JSON.stringify({ ptoken : ptoken }),
2427                 loaded);
2428
2429   wasm_promise = wasm_input
2430     .then(wasm_bindgen);
2431 }
2432
2433 function loaded(xhr: XMLHttpRequest){
2434   console.log('LOADED');
2435   var body = document.getElementById('loading_body')!;
2436   wasm_promise.then((got_wasm) => {
2437     wasm = got_wasm;
2438     body.outerHTML = xhr.response;
2439     try {
2440       startup();
2441     } catch (exc) {
2442       let s = exc.toString();
2443       string_report_error_raw('Exception on load, unrecoverable: ' + s);
2444     }
2445   });
2446 }
2447
2448 // todo scroll of log messages to bottom did not always work somehow
2449 //    think I have fixed this with approximation
2450
2451 //@@notest
2452 doload();