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