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