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