home · contact · privacy
Add basic admin commands.
[plomrogue2] / rogue_chat_nocanvas_monochrome.html
1 <!DOCTYPE html>
2 <html><head>
3 <style>
4 </style>
5 </head><body>
6 <div>
7 terminal rows: <input id="n_rows" type="number" step=4 min=24 value=24 />
8 terminal columns: <input id="n_cols" type="number" step=4 min=80 value=80 />
9 </div>
10 <pre id="terminal" style="display: inline-block;"></pre>
11 <textarea id="input" style="opacity: 0; width: 0px;"></textarea>
12 <div>
13 <h3>for mouse players</h3>
14 <table style="float: left">
15 <tr><td><button id="move_upleft">up-left</button></td><td><button id="move_up">up</button></td><td><button id="move_upright">up-right</button></td></tr>
16 <tr><td><button id="move_left">left</button></td><td>MOVE</td><td><button id="move_right">right</button></td></tr>
17 <tr><td><button id="move_downleft">down-left</button></td><td><button id="move_down">down</button></td><td><button id="move_downright">down-right</button></td></tr>
18 </table>
19 <div>
20 <button id="help">help</button>
21 <button id="switch_to_play">play mode</button>
22 <button id="switch_to_study">study mode</button>
23 <button id="switch_to_chat">chat mode</button><br />
24 <button id="take_thing">take thing</button>
25 <button id="drop_thing">drop thing</button>
26 <button id="flatten">flatten surroundings</button>
27 <button id="teleport">teleport</button>
28 <button id="switch_to_edit">change tile</button><br />
29 <button id="switch_to_password">change tile editing password</button>
30 <button id="switch_to_annotate">annotate tile</button>
31 <button id="switch_to_portal">edit portal link</button>
32 <button id="toggle_map_mode">toggle terrain/annotations/control view</button>
33 <button id="switch_to_admin">become admin</button>
34 <button id="switch_to_control_pw">change tile control password</button>
35 </div>
36 <h3>edit keybindings</h3> (see <a href="https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values">here</a> for non-obvious available values):<br />
37 <ul>
38 <li>move up (square grid): <input id="key_square_move_up" type="text" value="w" /> (hint: ArrowUp)
39 <li>move left (square grid): <input id="key_square_move_left" type="text" value="a" /> (hint: ArrowLeft)
40 <li>move down (square grid): <input id="key_square_move_down" type="text" value="s" /> (hint: ArrowDown)
41 <li>move right (square grid): <input id="key_square_move_right" type="text" value="d" /> (hint: ArrowRight)
42 <li>move up-left (hex grid): <input id="key_hex_move_upleft" type="text" value="w" />
43 <li>move up-right (hex grid): <input id="key_hex_move_upright" type="text" value="e" />
44 <li>move right (hex grid): <input id="key_hex_move_right" type="text" value="d" />
45 <li>move down-right (hex grid): <input id="key_hex_move_downright" type="text" value="x" />
46 <li>move down-left (hex grid): <input id="key_hex_move_downleft" type="text" value="y" />
47 <li>move left (hex grid): <input id="key_hex_move_left" type="text" value="a" />
48 <li>help: <input id="key_help" type="text" value="h" />
49 <li>flatten surroundings: <input id="key_flatten" type="text" value="F" />
50 <li>teleport: <input id="key_teleport" type="text" value="p" />
51 <li>take thing under player: <input id="key_take_thing" type="text" value="z" />
52 <li>drop carried thing: <input id="key_drop_thing" type="text" value="u" />
53 <li>switch to chat mode: <input id="key_switch_to_chat" type="text" value="t" />
54 <li>switch to play mode: <input id="key_switch_to_play" type="text" value="p" />
55 <li>switch to study mode: <input id="key_switch_to_study" type="text" value="?" />
56 <li>edit tile (from play mode): <input id="key_switch_to_edit" type="text" value="m" />
57 <li>enter tile password (from play mode): <input id="key_switch_to_password" type="text" value="P" />
58 <li>enter admin password (from play mode): <input id="key_switch_to_admin" type="text" value="A" />
59 <li>change tile control password (from play mode): <input id="key_switch_to_control_pw" type="text" value="C" />
60 <li>annotate tile (from play mode): <input id="key_switch_to_annotate" type="text" value="M" />
61 <li>annotate portal (from play mode): <input id="key_switch_to_portal" type="text" value="T" />
62 <li>toggle terrain/annotations/control view (from study mode): <input id="key_toggle_map_mode" type="text" value="M" />
63 </ul>
64 </div>
65 <script>
66 "use strict";
67 let websocket_location = "wss://plomlompom.com/rogue_chat/";
68 //let websocket_location = "ws://localhost:8000/";
69
70 let rows_selector = document.getElementById("n_rows");
71 let cols_selector = document.getElementById("n_cols");
72 let key_selectors = document.querySelectorAll('[id^="key_"]');
73
74 function restore_selector_value(selector) {
75     let stored_selection = window.localStorage.getItem(selector.id);
76     if (stored_selection) {
77         selector.value = stored_selection;
78     }
79 }
80 restore_selector_value(rows_selector);
81 restore_selector_value(cols_selector);
82 for (let key_selector of key_selectors) {
83     restore_selector_value(key_selector);
84 }
85
86 let terminal = {
87   foreground: 'white',
88   background: 'black',
89   initialize: function() {
90     this.rows = rows_selector.value;
91     this.cols = cols_selector.value;
92     this.pre_el = document.getElementById("terminal");
93     this.pre_el.style.color = this.foreground;
94     this.pre_el.style.backgroundColor = this.background;
95     this.content = [];
96       let line = []
97     for (let y = 0, x = 0; y <= this.rows; x++) {
98         if (x == this.cols) {
99             x = 0;
100             y += 1;
101             this.content.push(line);
102             line = [];
103             if (y == this.rows) {
104                 break;
105             }
106         }
107         line.push(' ');
108     }
109   },
110   blink_screen: function() {
111       this.pre_el.style.color = this.background;
112       this.pre_el.style.backgroundColor = this.foreground;
113       setTimeout(() => {
114           this.pre_el.style.color = this.foreground;
115           this.pre_el.style.backgroundColor = this.background;
116       }, 100);
117   },
118   refresh: function() {
119       let pre_string = '';
120       for (let y = 0; y < this.rows; y++) {
121           let line = this.content[y].join('');
122           pre_string += line + '\n';
123       }
124       this.pre_el.textContent = pre_string;
125   },
126   write: function(start_y, start_x, msg) {
127       for (let x = start_x, i = 0; x < this.cols && i < msg.length; x++, i++) {
128           this.content[start_y][x] = msg[i];
129       }
130   },
131   drawBox: function(start_y, start_x, height, width) {
132     let end_y = start_y + height;
133     let end_x = start_x + width;
134     for (let y = start_y, x = start_x; y < this.rows; x++) {
135       if (x == end_x) {
136         x = start_x;
137         y += 1;
138         if (y == end_y) {
139             break;
140         }
141       };
142       this.content[y][x] = ' ';
143     }
144   },
145 }
146 terminal.initialize();
147
148 let parser = {
149   tokenize: function(str) {
150     let tokens = [];
151     let token = ''
152     let quoted = false;
153     let escaped = false;
154     for (let i = 0; i < str.length; i++) {
155       let c = str[i];
156       if (quoted) {
157         if (escaped) {
158           token += c;
159           escaped = false;
160         } else if (c == '\\') {
161           escaped = true;
162         } else if (c == '"') {
163           quoted = false
164         } else {
165           token += c;
166         }
167       } else if (c == '"') {
168         quoted = true
169       } else if (c === ' ') {
170         if (token.length > 0) {
171           tokens.push(token);
172           token = '';
173         }
174       } else {
175         token += c;
176       }
177     }
178     if (token.length > 0) {
179       tokens.push(token);
180     }
181     return tokens;
182   },
183   parse_yx: function(position_string) {
184     let coordinate_strings = position_string.split(',')
185     let position = [0, 0];
186     position[0] = parseInt(coordinate_strings[0].slice(2));
187     position[1] = parseInt(coordinate_strings[1].slice(2));
188     return position;
189   },
190 }
191
192 class Thing {
193     constructor(yx) {
194         this.position = yx;
195     }
196 }
197
198 let server = {
199     init: function(url) {
200         this.url = url;
201         this.websocket = new WebSocket(this.url);
202         this.websocket.onopen = function(event) {
203             server.connected = true;
204             game.thing_types = {};
205             game.terrains = {};
206             server.send(['TASKS']);
207             server.send(['TERRAINS']);
208             server.send(['THING_TYPES']);
209             tui.log_msg("@ server connected! :)");
210             tui.switch_mode(mode_login);
211         };
212         this.websocket.onclose = function(event) {
213             server.connected = false;
214             tui.switch_mode(mode_waiting_for_server);
215             tui.log_msg("@ server disconnected :(");
216         };
217             this.websocket.onmessage = this.handle_event;
218         },
219     reconnect_to: function(url) {
220         this.websocket.close();
221         this.init(url);
222     },
223     send: function(tokens) {
224         this.websocket.send(unparser.untokenize(tokens));
225     },
226     handle_event: function(event) {
227         let tokens = parser.tokenize(event.data);
228         if (tokens[0] === 'TURN') {
229             game.turn_complete = false;
230             explorer.empty_info_db();
231             game.things = {};
232             game.portals = {};
233             game.turn = parseInt(tokens[1]);
234         } else if (tokens[0] === 'THING') {
235             let t = game.get_thing(tokens[3], true);
236             t.position = parser.parse_yx(tokens[1]);
237             t.type_ = tokens[2];
238         } else if (tokens[0] === 'THING_NAME') {
239             let t = game.get_thing(tokens[1], false);
240             if (t) {
241                 t.name_ = tokens[2];
242             };
243         } else if (tokens[0] === 'THING_CHAR') {
244             let t = game.get_thing(tokens[1], false);
245             if (t) {
246                 t.player_char = tokens[2];
247             };
248         } else if (tokens[0] === 'TASKS') {
249             game.tasks = tokens[1].split(',')
250         } else if (tokens[0] === 'THING_TYPE') {
251             game.thing_types[tokens[1]] = tokens[2]
252         } else if (tokens[0] === 'TERRAIN') {
253             game.terrains[tokens[1]] = tokens[2]
254         } else if (tokens[0] === 'MAP') {
255             game.map_geometry = tokens[1];
256             tui.init_keys();
257             game.map_size = parser.parse_yx(tokens[2]);
258             game.map = tokens[3]
259         } else if (tokens[0] === 'FOV') {
260             game.fov = tokens[1]
261         } else if (tokens[0] === 'MAP_CONTROL') {
262             game.map_control = tokens[1]
263         } else if (tokens[0] === 'GAME_STATE_COMPLETE') {
264             game.turn_complete = true;
265             if (tui.mode == mode_post_login_wait) {
266                 tui.switch_mode(mode_play);
267             } else if (tui.mode == mode_study) {
268                 explorer.query_info();
269             }
270             tui.full_refresh();
271         } else if (tokens[0] === 'CHAT') {
272              tui.log_msg('# ' + tokens[1], 1);
273         } else if (tokens[0] === 'PLAYER_ID') {
274             game.player_id = parseInt(tokens[1]);
275         } else if (tokens[0] === 'LOGIN_OK') {
276             this.send(['GET_GAMESTATE']);
277             tui.switch_mode(mode_post_login_wait);
278         } else if (tokens[0] === 'PORTAL') {
279             let position = parser.parse_yx(tokens[1]);
280             game.portals[position] = tokens[2];
281         } else if (tokens[0] === 'ANNOTATION_HINT') {
282             let position = parser.parse_yx(tokens[1]);
283             explorer.info_hints = explorer.info_hints.concat([position]);
284         } else if (tokens[0] === 'ANNOTATION') {
285             let position = parser.parse_yx(tokens[1]);
286             explorer.update_info_db(position, tokens[2]);
287             tui.restore_input_values();
288             tui.full_refresh();
289         } else if (tokens[0] === 'UNHANDLED_INPUT') {
290             tui.log_msg('? unknown command');
291         } else if (tokens[0] === 'PLAY_ERROR') {
292             tui.log_msg('? ' + tokens[1]);
293             terminal.blink_screen();
294         } else if (tokens[0] === 'ARGUMENT_ERROR') {
295             tui.log_msg('? syntax error: ' + tokens[1]);
296         } else if (tokens[0] === 'GAME_ERROR') {
297             tui.log_msg('? game error: ' + tokens[1]);
298         } else if (tokens[0] === 'PONG') {
299             ;
300         } else {
301             tui.log_msg('? unhandled input: ' + event.data);
302         }
303     }
304 }
305
306 let unparser = {
307     quote: function(str) {
308         let quoted = ['"'];
309         for (let i = 0; i < str.length; i++) {
310             let c = str[i];
311             if (['"', '\\'].includes(c)) {
312                 quoted.push('\\');
313             };
314             quoted.push(c);
315         }
316         quoted.push('"');
317         return quoted.join('');
318     },
319     to_yx: function(yx_coordinate) {
320         return "Y:" + yx_coordinate[0] + ",X:" + yx_coordinate[1];
321     },
322     untokenize: function(tokens) {
323         let quoted_tokens = [];
324         for (let token of tokens) {
325             quoted_tokens.push(this.quote(token));
326         }
327         return quoted_tokens.join(" ");
328     }
329 }
330
331 class Mode {
332     constructor(name, help_intro, has_input_prompt=false, shows_info=false,
333                 is_intro=false, is_single_char_entry=false) {
334         this.name = name;
335         this.has_input_prompt = has_input_prompt;
336         this.shows_info= shows_info;
337         this.is_intro = is_intro;
338         this.help_intro = help_intro;
339         this.is_single_char_entry = is_single_char_entry;
340     }
341 }
342   let mode_waiting_for_server = new Mode('waiting_for_server',
343                                          'Waiting for a server response.',
344                                          false, false, true);
345   let mode_login = new Mode('login',
346                             'Pick your player name.',
347                             true, false, true);
348   let mode_post_login_wait = new Mode('waiting for game world',
349                                       'Waiting for a server response.')
350   let mode_chat = new Mode('chat',
351                            'This mode allows you to engage in chit-chat with other users.  Any line you enter into the input prompt that does not start with a "/" will be sent out to nearby players – but barriers and distance will reduce what they can read, so stand close to them to ensure they get your message.  Lines that start with a "/" are used for commands like:',
352                            true);
353   let mode_annotate = new Mode('annotate',
354                                'This mode allows you to add/edit a comment on the tile you are currently standing on (provided your map editing password authorizes you so).  Hit Return to leave.',
355                                true, true);
356   let mode_play = new Mode('play',
357                            'This mode allows you to interact with the map.')
358   let mode_study = new Mode('study',
359                             'This mode allows you to study the map and its tiles in detail.  Move the question mark over a tile, and the right half of the screen will show detailed information on it.',
360                             false, true);
361   let mode_edit = new Mode('edit',
362                            'This mode allows you to change the map tile you currently stand on (if your map editing password authorizes you so).  Just enter any printable ASCII character to imprint it on the ground below you.',
363                            false, false, false, true);
364   let mode_control_pw_type = new Mode('control_pw_type',
365                                       'This mode is the first of two steps to change the password for a tile control character.  First enter the tile control character for which you want to change the password!',
366                                       false, false, false, true);
367   let mode_portal = new Mode('portal',
368                              'This mode allows you to imprint/edit/remove a teleportation target on the ground you are currently standing on (provided your map editing password authorizes you so).  Enter or edit a URL to imprint a teleportation target; enter emptiness to remove a pre-existing teleportation target.  Hit Return to leave.',
369                              true, true);
370   let mode_password = new Mode('password',
371                                'This mode allows you to change the password that you send to authorize yourself for editing password-protected map tiles.  Hit return to confirm and leave.',
372                                true);
373   let mode_admin = new Mode('admin',
374                             'This mode allows you to become admin if you know an admin password.',
375                             true);
376   let mode_control_pw_pw = new Mode('control_pw_pw',
377                                     'This mode is the second of two steps to change the password for a tile control character.  Enter the new password for the tile control character you chose.',
378                                     true);
379
380 let tui = {
381   mode: mode_waiting_for_server,
382   log: [],
383   input_prompt: '> ',
384   input_lines: [],
385   window_width: terminal.cols / 2,
386   height_turn_line: 1,
387   height_mode_line: 1,
388   height_input: 1,
389   password: 'foo',
390   show_help: false,
391   init: function() {
392       this.inputEl = document.getElementById("input");
393       this.inputEl.focus();
394       this.recalc_input_lines();
395       this.height_header = this.height_turn_line + this.height_mode_line;
396       this.log_msg("@ waiting for server connection ...");
397       this.init_keys();
398   },
399   init_keys: function() {
400     this.keys = {};
401     for (let key_selector of key_selectors) {
402         this.keys[key_selector.id.slice(4)] = key_selector.value;
403     }
404     this.movement_keys = {
405         [this.keys.square_move_up]: 'UP',
406         [this.keys.square_move_left]: 'LEFT',
407         [this.keys.square_move_down]: 'DOWN',
408         [this.keys.square_move_right]: 'RIGHT'
409     };
410     if (game.map_geometry == 'Hex') {
411         this.movement_keys = {
412             [this.keys.hex_move_upleft]: 'UPLEFT',
413             [this.keys.hex_move_upright]: 'UPRIGHT',
414             [this.keys.hex_move_right]: 'RIGHT',
415             [this.keys.hex_move_downright]: 'DOWNRIGHT',
416             [this.keys.hex_move_downleft]: 'DOWNLEFT',
417             [this.keys.hex_move_left]: 'LEFT'
418         };
419     };
420   },
421   switch_mode: function(mode) {
422     this.inputEl.focus();
423     //this.show_help = false;
424     this.map_mode = 'terrain';
425     if (mode.shows_info && game.player_id in game.things) {
426       explorer.position = game.things[game.player_id].position;
427       explorer.query_info();
428     }
429     this.mode = mode;
430     this.empty_input();
431     this.restore_input_values();
432     document.getElementById("take_thing").disabled = true;
433     document.getElementById("drop_thing").disabled = true;
434     document.getElementById("flatten").disabled = true;
435     document.getElementById("teleport").disabled = true;
436     document.getElementById("toggle_map_mode").disabled = true;
437     document.getElementById("switch_to_chat").disabled = true;
438     document.getElementById("switch_to_play").disabled = true;
439     document.getElementById("switch_to_study").disabled = true;
440     document.getElementById("switch_to_edit").disabled = true;
441     document.getElementById("switch_to_portal").disabled = true;
442     document.getElementById("switch_to_annotate").disabled = true;
443     document.getElementById("switch_to_password").disabled = true;
444     document.getElementById("switch_to_admin").disabled = true;
445     document.getElementById("switch_to_control_pw").disabled = true;
446     document.getElementById("move_left").disabled = true;
447     document.getElementById("move_upleft").disabled = true;
448     document.getElementById("move_up").disabled = true;
449     document.getElementById("move_upright").disabled = true;
450     document.getElementById("move_downleft").disabled = true;
451     document.getElementById("move_down").disabled = true;
452     document.getElementById("move_downright").disabled = true;
453     document.getElementById("move_right").disabled = true;
454     if (mode == mode_play || mode == mode_study) {
455         document.getElementById("move_left").disabled = false;
456         document.getElementById("move_right").disabled = false;
457         if (game.map_geometry == 'Hex') {
458             document.getElementById("move_upleft").disabled = false;
459             document.getElementById("move_upright").disabled = false;
460             document.getElementById("move_downleft").disabled = false;
461             document.getElementById("move_downright").disabled = false;
462         } else {
463             document.getElementById("move_up").disabled = false;
464             document.getElementById("move_down").disabled = false;
465         }
466     }
467     if (!mode.is_intro && mode != mode_play) {
468         document.getElementById("switch_to_play").disabled = false;
469     }
470     if (!mode.is_intro && mode != mode_study) {
471         document.getElementById("switch_to_study").disabled = false;
472     }
473     if (!mode.is_intro && mode != mode_chat) {
474         document.getElementById("switch_to_chat").disabled = false;
475     }
476     if (mode == mode_login) {
477         if (this.login_name) {
478             server.send(['LOGIN', this.login_name]);
479         } else {
480             this.log_msg("? need login name");
481         }
482     } else if (mode == mode_play) {
483         if (game.tasks.includes('PICK_UP')) {
484             document.getElementById("take_thing").disabled = false;
485         }
486         if (game.tasks.includes('DROP')) {
487             document.getElementById("drop_thing").disabled = false;
488         }
489         if (game.tasks.includes('FLATTEN_SURROUNDINGS')) {
490             document.getElementById("flatten").disabled = false;
491         }
492         if (game.tasks.includes('MOVE')) {
493         }
494         document.getElementById("teleport").disabled = false;
495         document.getElementById("switch_to_annotate").disabled = false;
496         document.getElementById("switch_to_edit").disabled = false;
497         document.getElementById("switch_to_portal").disabled = false;
498         document.getElementById("switch_to_password").disabled = false;
499         document.getElementById("switch_to_admin").disabled = false;
500         document.getElementById("switch_to_control_pw").disabled = false;
501     } else if (mode == mode_study) {
502         document.getElementById("toggle_map_mode").disabled = false;
503     } else if (mode.is_single_char_entry) {
504         this.show_help = true;
505     } else if (mode == mode_admin) {
506         this.log_msg('@ enter admin password:')
507     } else if (mode == mode_control_pw_pw) {
508         this.log_msg('@ enter tile control password for "' + this.tile_control_char + '":');
509     }
510     this.full_refresh();
511   },
512   restore_input_values: function() {
513       if (this.mode == mode_annotate && explorer.position in explorer.info_db) {
514           let info = explorer.info_db[explorer.position];
515           if (info != "(none)") {
516               this.inputEl.value = info;
517               this.recalc_input_lines();
518           }
519       } else if (this.mode == mode_portal && explorer.position in game.portals) {
520           let portal = game.portals[explorer.position]
521           this.inputEl.value = portal;
522           this.recalc_input_lines();
523       } else if (this.mode == mode_password) {
524           this.inputEl.value = this.password;
525           this.recalc_input_lines();
526       }
527   },
528   empty_input: function(str) {
529       this.inputEl.value = "";
530       if (this.mode.has_input_prompt) {
531           this.recalc_input_lines();
532       } else {
533           this.height_input = 0;
534       }
535   },
536   recalc_input_lines: function() {
537       this.input_lines = this.msg_into_lines_of_width(this.input_prompt + this.inputEl.value, this.window_width);
538       this.height_input = this.input_lines.length;
539   },
540   msg_into_lines_of_width: function(msg, width) {
541     let chunk = "";
542     let lines = [];
543     for (let i = 0, x = 0; i < msg.length; i++, x++) {
544       if (x >= width || msg[i] == "\n") {
545         lines.push(chunk);
546         chunk = "";
547         x = 0;
548       };
549       if (msg[i] != "\n") {
550         chunk += msg[i];
551       }
552     }
553     lines.push(chunk);
554     return lines;
555   },
556   log_msg: function(msg) {
557       this.log.push(msg);
558       while (this.log.length > 100) {
559         this.log.shift();
560       };
561       this.full_refresh();
562   },
563   draw_map: function() {
564     let map_lines_split = [];
565     let line = [];
566     let map_content = game.map;
567     if (this.map_mode == 'control') {
568         map_content = game.map_control;
569     }
570     for (let i = 0, j = 0; i < game.map.length; i++, j++) {
571         if (j == game.map_size[1]) {
572             map_lines_split.push(line);
573             line = [];
574             j = 0;
575         };
576         line.push(map_content[i] + ' ');
577     };
578     map_lines_split.push(line);
579     if (this.map_mode == 'annotations') {
580         for (const coordinate of explorer.info_hints) {
581             map_lines_split[coordinate[0]][coordinate[1]] = 'A ';
582         }
583     } else if (this.map_mode == 'terrain') {
584         for (const p in game.portals) {
585             let coordinate = p.split(',')
586             map_lines_split[coordinate[0]][coordinate[1]] = 'P ';
587         }
588         let used_positions = [];
589         for (const thing_id in game.things) {
590             let t = game.things[thing_id];
591             let symbol = game.thing_types[t.type_];
592             let meta_char = ' ';
593             if (t.player_char) {
594                 meta_char = t.player_char;
595             }
596             if (used_positions.includes(t.position.toString())) {
597                 meta_char = '+';
598             };
599             map_lines_split[t.position[0]][t.position[1]] = symbol + meta_char;
600             used_positions.push(t.position.toString());
601         };
602     }
603     if (tui.mode.shows_info) {
604         map_lines_split[explorer.position[0]][explorer.position[1]] = '??';
605     }
606     let map_lines = []
607     if (game.map_geometry == 'Square') {
608         for (let line_split of map_lines_split) {
609             map_lines.push(line_split.join(''));
610         };
611     } else if (game.map_geometry == 'Hex') {
612         let indent = 0
613         for (let line_split of map_lines_split) {
614             map_lines.push(' '.repeat(indent) + line_split.join(''));
615             if (indent == 0) {
616                 indent = 1;
617             } else {
618                 indent = 0;
619             };
620         };
621     }
622     let window_center = [terminal.rows / 2, this.window_width / 2];
623     let player = game.things[game.player_id];
624     let center_position = [player.position[0], player.position[1]];
625     if (tui.mode.shows_info) {
626         center_position = [explorer.position[0], explorer.position[1]];
627     }
628     center_position[1] = center_position[1] * 2;
629     let offset = [center_position[0] - window_center[0],
630                   center_position[1] - window_center[1]]
631     if (game.map_geometry == 'Hex' && offset[0] % 2) {
632         offset[1] += 1;
633     };
634     let term_y = Math.max(0, -offset[0]);
635     let term_x = Math.max(0, -offset[1]);
636     let map_y = Math.max(0, offset[0]);
637     let map_x = Math.max(0, offset[1]);
638     for (; term_y < terminal.rows && map_y < game.map_size[0]; term_y++, map_y++) {
639         let to_draw = map_lines[map_y].slice(map_x, this.window_width + offset[1]);
640         terminal.write(term_y, term_x, to_draw);
641     }
642   },
643   draw_mode_line: function() {
644       let help = 'hit [' + this.keys.help + '] for help';
645       if (this.mode.has_input_prompt) {
646           help = 'enter /help for help';
647       }
648       terminal.write(0, this.window_width, 'MODE: ' + this.mode.name + ' – ' + help);
649   },
650   draw_turn_line: function(n) {
651     terminal.write(1, this.window_width, 'TURN: ' + game.turn);
652   },
653   draw_history: function() {
654       let log_display_lines = [];
655       for (let line of this.log) {
656           log_display_lines = log_display_lines.concat(this.msg_into_lines_of_width(line, this.window_width));
657       };
658       for (let y = terminal.rows - 1 - this.height_input,
659                i = log_display_lines.length - 1;
660            y >= this.height_header && i >= 0;
661            y--, i--) {
662           terminal.write(y, this.window_width, log_display_lines[i]);
663       }
664   },
665   draw_info: function() {
666     let lines = this.msg_into_lines_of_width(explorer.get_info(), this.window_width);
667     for (let y = this.height_header, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
668       terminal.write(y, this.window_width, lines[i]);
669     }
670   },
671   draw_input: function() {
672     if (this.mode.has_input_prompt) {
673         for (let y = terminal.rows - this.height_input, i = 0; i < this.input_lines.length; y++, i++) {
674             terminal.write(y, this.window_width, this.input_lines[i]);
675         }
676     }
677   },
678   draw_help: function() {
679       let movement_keys_desc = Object.keys(this.movement_keys).join(',');
680       let content = this.mode.name + " mode help\n\n" + this.mode.help_intro + "\n\n";
681       if (this.mode == mode_play) {
682           content += "Available actions:\n";
683           if (game.tasks.includes('MOVE')) {
684               content += "[" + movement_keys_desc + "] – move player\n";
685           }
686           if (game.tasks.includes('PICK_UP')) {
687               content += "[" + this.keys.take_thing + "] – take thing under player\n";
688           }
689           if (game.tasks.includes('DROP')) {
690               content += "[" + this.keys.drop_thing + "] – drop carried thing\n";
691           }
692           if (game.tasks.includes('FLATTEN_SURROUNDINGS')) {
693               content += "[" + tui.keys.flatten + "] – flatten player's surroundings\n";
694           }
695           content += "[" + tui.keys.teleport + "] – teleport to other space\n";
696           content += '\nOther modes available from here:\n';
697           content += '[' + this.keys.switch_to_chat + '] – chat mode\n';
698           content += '[' + this.keys.switch_to_study + '] – study mode\n';
699           content += '[' + this.keys.switch_to_edit + '] – terrain edit mode\n';
700           content += '[' + this.keys.switch_to_portal + '] – portal edit mode\n';
701           content += '[' + this.keys.switch_to_annotate + '] – annotation mode\n';
702           content += '[' + this.keys.switch_to_password + '] – password input mode\n';
703           content += '[' + this.keys.switch_to_admin + '] – become admin\n';
704           content += '[' + this.keys.switch_to_control_pw + '] – change tile control password\n';
705       } else if (this.mode == mode_study) {
706           content += "Available actions:\n";
707           content += '[' + movement_keys_desc + '] – move question mark\n';
708           content += '[' + this.keys.toggle_map_mode + '] – toggle view between terrain, annotations, and password protection areas\n';
709           content += '\nOther modes available from here:\n';
710           content += '[' + this.keys.switch_to_chat + '] – chat mode\n';
711           content += '[' + this.keys.switch_to_play + '] – play mode\n';
712       } else if (this.mode == mode_chat) {
713           content += '/nick NAME – re-name yourself to NAME\n';
714           content += '/' + this.keys.switch_to_play + ' or /play – switch to play mode\n';
715           content += '/' + this.keys.switch_to_study + ' or /study – switch to study mode\n';
716       }
717       let start_x = 0;
718       if (!this.mode.has_input_prompt) {
719           start_x = this.window_width
720       }
721       terminal.drawBox(0, start_x, terminal.rows, this.window_width);
722       let lines = this.msg_into_lines_of_width(content, this.window_width);
723       for (let y = 0, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
724           terminal.write(y, start_x, lines[i]);
725       }
726   },
727   full_refresh: function() {
728     terminal.drawBox(0, 0, terminal.rows, terminal.cols);
729     if (this.mode.is_intro) {
730         this.draw_history();
731         this.draw_input();
732     } else {
733         if (game.turn_complete) {
734             this.draw_map();
735             this.draw_turn_line();
736         }
737         this.draw_mode_line();
738         if (this.mode.shows_info) {
739           this.draw_info();
740         } else {
741           this.draw_history();
742         }
743         this.draw_input();
744     }
745     if (this.show_help) {
746         this.draw_help();
747     }
748     terminal.refresh();
749   }
750 }
751
752 let game = {
753     init: function() {
754         this.things = {};
755         this.turn = -1;
756         this.map = "";
757         this.map_control = "";
758         this.map_size = [0,0];
759         this.player_id = -1;
760         this.portals = {};
761         this.tasks = {};
762     },
763     get_thing: function(id_, create_if_not_found=false) {
764         if (id_ in game.things) {
765             return game.things[id_];
766         } else if (create_if_not_found) {
767             let t = new Thing([0,0]);
768             game.things[id_] = t;
769             return t;
770         };
771     },
772     move: function(start_position, direction) {
773         let target = [start_position[0], start_position[1]];
774         if (direction == 'LEFT') {
775             target[1] -= 1;
776         } else if (direction == 'RIGHT') {
777             target[1] += 1;
778         } else if (game.map_geometry == 'Square') {
779             if (direction == 'UP') {
780                 target[0] -= 1;
781             } else if (direction == 'DOWN') {
782                 target[0] += 1;
783             };
784         } else if (game.map_geometry == 'Hex') {
785             let start_indented = start_position[0] % 2;
786             if (direction == 'UPLEFT') {
787                 target[0] -= 1;
788                 if (!start_indented) {
789                     target[1] -= 1;
790                 }
791             } else if (direction == 'UPRIGHT') {
792                 target[0] -= 1;
793                 if (start_indented) {
794                     target[1] += 1;
795                 }
796             } else if (direction == 'DOWNLEFT') {
797                 target[0] += 1;
798                 if (!start_indented) {
799                     target[1] -= 1;
800                 }
801             } else if (direction == 'DOWNRIGHT') {
802                 target[0] += 1;
803                 if (start_indented) {
804                     target[1] += 1;
805                 }
806             };
807         };
808         if (target[0] < 0 || target[1] < 0 ||
809             target[0] >= this.map_size[0] || target[1] >= this.map_size[1]) {
810             return null;
811         };
812         return target;
813     },
814     teleport: function() {
815         let player = this.get_thing(game.player_id);
816         if (player.position in this.portals) {
817             server.reconnect_to(this.portals[player.position]);
818         } else {
819             terminal.blink_screen();
820             tui.log_msg('? not standing on portal')
821         }
822     }
823 }
824
825 game.init();
826 tui.init();
827 tui.full_refresh();
828 server.init(websocket_location);
829
830 let explorer = {
831     position: [0,0],
832     info_db: {},
833     info_hints: [],
834     move: function(direction) {
835         let target = game.move(this.position, direction);
836         if (target) {
837             this.position = target
838             this.query_info();
839         } else {
840             terminal.blink_screen();
841         };
842     },
843     update_info_db: function(yx, str) {
844         this.info_db[yx] = str;
845         if (tui.mode == mode_study) {
846             tui.full_refresh();
847         }
848     },
849     empty_info_db: function() {
850         this.info_db = {};
851         this.info_hints = [];
852         if (tui.mode == mode_study) {
853             tui.full_refresh();
854         }
855     },
856     query_info: function() {
857         server.send(["GET_ANNOTATION", unparser.to_yx(explorer.position)]);
858     },
859     get_info: function() {
860         let position_i = this.position[0] * game.map_size[1] + this.position[1];
861         if (game.fov[position_i] != '.') {
862             return 'outside field of view';
863         };
864         let info = "";
865         let terrain_char = game.map[position_i]
866         let terrain_desc = '?'
867         if (game.terrains[terrain_char]) {
868             terrain_desc = game.terrains[terrain_char];
869         };
870         info += 'TERRAIN: "' + terrain_char + '" / ' + terrain_desc + "\n";
871         let protection = game.map_control[position_i];
872         if (protection == '.') {
873             protection = 'unprotected';
874         };
875         info += 'PROTECTION: ' + protection + '\n';
876         for (let t_id in game.things) {
877              let t = game.things[t_id];
878              if (t.position[0] == this.position[0] && t.position[1] == this.position[1]) {
879                  let symbol = game.thing_types[t.type_];
880                  info += "THING: " + t.type_ + " / " + symbol;
881                  if (t.player_char) {
882                      info += t.player_char;
883                  };
884                  if (t.name_) {
885                      info += " (" + t.name_ + ")";
886                  }
887                  info += "\n";
888              }
889         }
890         if (this.position in game.portals) {
891             info += "PORTAL: " + game.portals[this.position] + "\n";
892         }
893         if (this.position in this.info_db) {
894             info += "ANNOTATIONS: " + this.info_db[this.position];
895         } else {
896             info += 'waiting …';
897         }
898         return info;
899     },
900     annotate: function(msg) {
901         if (msg.length == 0) {
902             msg = " ";  // triggers annotation deletion
903         }
904         server.send(["ANNOTATE", unparser.to_yx(explorer.position), msg, tui.password]);
905     },
906     set_portal: function(msg) {
907         if (msg.length == 0) {
908             msg = " ";  // triggers portal deletion
909         }
910         server.send(["PORTAL", unparser.to_yx(explorer.position), msg, tui.password]);
911     }
912 }
913
914 tui.inputEl.addEventListener('input', (event) => {
915     if (tui.mode.has_input_prompt) {
916         let max_length = tui.window_width * terminal.rows - tui.input_prompt.length;
917         if (tui.inputEl.value.length > max_length) {
918             tui.inputEl.value = tui.inputEl.value.slice(0, max_length);
919         };
920         tui.recalc_input_lines();
921     } else if (tui.mode == mode_edit && tui.inputEl.value.length > 0) {
922         server.send(["TASK:WRITE", tui.inputEl.value[0], tui.password]);
923         tui.switch_mode(mode_play);
924     } else if (tui.mode == mode_control_pw_type && tui.inputEl.value.length > 0) {
925         tui.tile_control_char = tui.inputEl.value[0];
926         tui.switch_mode(mode_control_pw_pw);
927     }
928     tui.full_refresh();
929 }, false);
930 tui.inputEl.addEventListener('keydown', (event) => {
931     tui.show_help = false;
932     if (event.key == 'Enter') {
933         event.preventDefault();
934     }
935     if (tui.mode.has_input_prompt && event.key == 'Enter' && tui.inputEl.value == '/help') {
936         tui.show_help = true;
937         tui.empty_input();
938         tui.restore_input_values();
939     } else if (!tui.mode.has_input_prompt && event.key == tui.keys.help
940                && !tui.mode.is_single_char_entry) {
941         tui.show_help = true;
942     } else if (tui.mode == mode_login && event.key == 'Enter') {
943         tui.login_name = tui.inputEl.value;
944         server.send(['LOGIN', tui.inputEl.value]);
945         tui.empty_input();
946     } else if (tui.mode == mode_control_pw_pw && event.key == 'Enter') {
947         if (tui.inputEl.value.length == 0) {
948             tui.log_msg('@ aborted');
949         } else {
950             server.send(['SET_MAP_CONTROL_PASSWORD',
951                         tui.tile_control_char, tui.inputEl.value]);
952         }
953         tui.switch_mode(mode_play);
954     } else if (tui.mode == mode_portal && event.key == 'Enter') {
955         explorer.set_portal(tui.inputEl.value);
956         tui.switch_mode(mode_play);
957     } else if (tui.mode == mode_annotate && event.key == 'Enter') {
958         explorer.annotate(tui.inputEl.value);
959         tui.switch_mode(mode_play);
960     } else if (tui.mode == mode_password && event.key == 'Enter') {
961         if (tui.inputEl.value.length == 0) {
962             tui.inputEl.value = " ";
963         }
964         tui.password = tui.inputEl.value
965         tui.switch_mode(mode_play);
966     } else if (tui.mode == mode_admin && event.key == 'Enter') {
967         server.send(['BECOME_ADMIN', tui.inputEl.value]);
968         tui.switch_mode(mode_play);
969     } else if (tui.mode == mode_chat && event.key == 'Enter') {
970         let tokens = parser.tokenize(tui.inputEl.value);
971         if (tokens.length > 0 && tokens[0].length > 0) {
972             if (tui.inputEl.value[0][0] == '/') {
973                 if (tokens[0].slice(1) == 'play' || tokens[0][1] == tui.keys.switch_to_play) {
974                     tui.switch_mode(mode_play);
975                 } else if (tokens[0].slice(1) == 'study' || tokens[0][1] == tui.keys.switch_to_study) {
976                     tui.switch_mode(mode_study);
977                 } else if (tokens[0].slice(1) == 'nick') {
978                     if (tokens.length > 1) {
979                         server.send(['NICK', tokens[1]]);
980                     } else {
981                         tui.log_msg('? need new name');
982                     }
983                 } else {
984                     tui.log_msg('? unknown command');
985                 }
986             } else {
987                     server.send(['ALL', tui.inputEl.value]);
988             }
989         } else if (tui.inputEl.valuelength > 0) {
990                 server.send(['ALL', tui.inputEl.value]);
991         }
992         tui.empty_input();
993     } else if (tui.mode == mode_play) {
994           if (event.key === tui.keys.switch_to_chat) {
995               event.preventDefault();
996               tui.switch_mode(mode_chat);
997           } else if (event.key === tui.keys.switch_to_edit
998                      && game.tasks.includes('WRITE')) {
999               event.preventDefault();
1000               tui.switch_mode(mode_edit);
1001           } else if (event.key === tui.keys.switch_to_study) {
1002               tui.switch_mode(mode_study);
1003           } else if (event.key === tui.keys.switch_to_admin) {
1004               event.preventDefault();
1005               tui.switch_mode(mode_admin);
1006           } else if (event.key === tui.keys.switch_to_control_pw) {
1007               event.preventDefault();
1008               tui.switch_mode(mode_control_pw_type);
1009           } else if (event.key === tui.keys.switch_to_password) {
1010               event.preventDefault();
1011               tui.switch_mode(mode_password);
1012           } else if (event.key === tui.keys.flatten
1013                      && game.tasks.includes('FLATTEN_SURROUNDINGS')) {
1014               server.send(["TASK:FLATTEN_SURROUNDINGS", tui.password]);
1015           } else if (event.key === tui.keys.take_thing
1016                      && game.tasks.includes('PICK_UP')) {
1017               server.send(["TASK:PICK_UP"]);
1018           } else if (event.key === tui.keys.drop_thing
1019                      && game.tasks.includes('DROP')) {
1020               server.send(["TASK:DROP"]);
1021           } else if (event.key in tui.movement_keys
1022                      && game.tasks.includes('MOVE')) {
1023               server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1024           } else if (event.key === tui.keys.teleport) {
1025               game.teleport();
1026           } else if (event.key === tui.keys.switch_to_portal) {
1027               event.preventDefault();
1028               tui.switch_mode(mode_portal);
1029           } else if (event.key === tui.keys.switch_to_annotate) {
1030               event.preventDefault();
1031               tui.switch_mode(mode_annotate);
1032           };
1033     } else if (tui.mode == mode_study) {
1034         if (event.key === tui.keys.switch_to_chat) {
1035             event.preventDefault();
1036             tui.switch_mode(mode_chat);
1037         } else if (event.key == tui.keys.switch_to_play) {
1038             tui.switch_mode(mode_play);
1039         } else if (event.key in tui.movement_keys) {
1040             explorer.move(tui.movement_keys[event.key]);
1041         } else if (event.key == tui.keys.toggle_map_mode) {
1042             if (tui.map_mode == 'terrain') {
1043                 tui.map_mode = 'annotations';
1044             } else if (tui.map_mode == 'annotations') {
1045                 tui.map_mode = 'control';
1046             } else {
1047                 tui.map_mode = 'terrain';
1048             }
1049         };
1050     }
1051     tui.full_refresh();
1052 }, false);
1053
1054 rows_selector.addEventListener('input', function() {
1055     if (rows_selector.value % 4 != 0 || rows_selector.value < 24) {
1056         return;
1057     }
1058     window.localStorage.setItem(rows_selector.id, rows_selector.value);
1059     terminal.initialize();
1060     tui.full_refresh();
1061 }, false);
1062 cols_selector.addEventListener('input', function() {
1063     if (cols_selector.value % 4 != 0 || cols_selector.value < 80) {
1064         return;
1065     }
1066     window.localStorage.setItem(cols_selector.id, cols_selector.value);
1067     terminal.initialize();
1068     tui.window_width = terminal.cols / 2,
1069     tui.full_refresh();
1070 }, false);
1071 for (let key_selector of key_selectors) {
1072     key_selector.addEventListener('input', function() {
1073         window.localStorage.setItem(key_selector.id, key_selector.value);
1074         tui.init_keys();
1075     }, false);
1076 }
1077 window.setInterval(function() {
1078     if (server.connected) {
1079         server.send(['PING']);
1080     } else {
1081         server.reconnect_to(server.url);
1082         tui.log_msg('@ attempting reconnect …')
1083     }
1084 }, 5000);
1085 document.getElementById("terminal").onclick = function() {
1086     tui.inputEl.focus();
1087 };
1088 document.getElementById("help").onclick = function() {
1089     tui.show_help = true;
1090     tui.full_refresh();
1091 };
1092 document.getElementById("switch_to_play").onclick = function() {
1093     tui.switch_mode(mode_play);
1094     tui.full_refresh();
1095 };
1096 document.getElementById("switch_to_study").onclick = function() {
1097     tui.switch_mode(mode_study);
1098     tui.full_refresh();
1099 };
1100 document.getElementById("switch_to_chat").onclick = function() {
1101     tui.switch_mode(mode_chat);
1102     tui.full_refresh();
1103 };
1104 document.getElementById("switch_to_password").onclick = function() {
1105     tui.switch_mode(mode_password);
1106     tui.full_refresh();
1107 };
1108 document.getElementById("switch_to_edit").onclick = function() {
1109     tui.switch_mode(mode_edit);
1110     tui.full_refresh();
1111 };
1112 document.getElementById("switch_to_annotate").onclick = function() {
1113     tui.switch_mode(mode_annotate);
1114     tui.full_refresh();
1115 };
1116 document.getElementById("switch_to_portal").onclick = function() {
1117     tui.switch_mode(mode_portal);
1118     tui.full_refresh();
1119 };
1120 document.getElementById("switch_to_admin").onclick = function() {
1121     tui.switch_mode(mode_admin);
1122     tui.full_refresh();
1123 };
1124 document.getElementById("switch_to_control_pw").onclick = function() {
1125     tui.switch_mode(mode_control_pw_type);
1126     tui.full_refresh();
1127 };
1128 document.getElementById("toggle_map_mode").onclick = function() {
1129     if (tui.map_mode == 'terrain') {
1130         tui.map_mode = 'annotations';
1131     } else if (tui.map_mode == 'annotations') {
1132         tui.map_mode = 'control';
1133     } else {
1134         tui.map_mode = 'terrain';
1135     }
1136     tui.full_refresh();
1137 };
1138 document.getElementById("take_thing").onclick = function() {
1139         server.send(['TASK:PICK_UP']);
1140 };
1141 document.getElementById("drop_thing").onclick = function() {
1142         server.send(['TASK:DROP']);
1143 };
1144 document.getElementById("flatten").onclick = function() {
1145     server.send(['TASK:FLATTEN_SURROUNDINGS', tui.password]);
1146 };
1147 document.getElementById("teleport").onclick = function() {
1148     game.teleport();
1149 };
1150 document.getElementById("move_upleft").onclick = function() {
1151     if (tui.mode == mode_play) {
1152         server.send(['TASK:MOVE', 'UPLEFT']);
1153     } else {
1154         explorer.move('UPLEFT');
1155     };
1156 };
1157 document.getElementById("move_left").onclick = function() {
1158     if (tui.mode == mode_play) {
1159         server.send(['TASK:MOVE', 'LEFT']);
1160     } else {
1161         explorer.move('LEFT');
1162     };
1163 };
1164 document.getElementById("move_downleft").onclick = function() {
1165     if (tui.mode == mode_play) {
1166         server.send(['TASK:MOVE', 'DOWNLEFT']);
1167     } else {
1168         explorer.move('DOWNLEFT');
1169     };
1170 };
1171 document.getElementById("move_down").onclick = function() {
1172     if (tui.mode == mode_play) {
1173         server.send(['TASK:MOVE', 'DOWN']);
1174     } else {
1175         explorer.move('DOWN');
1176     };
1177 };
1178 document.getElementById("move_up").onclick = function() {
1179     if (tui.mode == mode_play) {
1180         server.send(['TASK:MOVE', 'UP']);
1181     } else {
1182         explorer.move('UP');
1183     };
1184 };
1185 document.getElementById("move_upright").onclick = function() {
1186     if (tui.mode == mode_play) {
1187         server.send(['TASK:MOVE', 'UPRIGHT']);
1188     } else {
1189         explorer.move('UPRIGHT');
1190     };
1191 };
1192 document.getElementById("move_right").onclick = function() {
1193     if (tui.mode == mode_play) {
1194         server.send(['TASK:MOVE', 'RIGHT']);
1195     } else {
1196         explorer.move('RIGHT');
1197     };
1198 };
1199 document.getElementById("move_downright").onclick = function() {
1200     if (tui.mode == mode_play) {
1201         server.send(['TASK:MOVE', 'DOWNRIGHT']);
1202     } else {
1203         explorer.move('DOWNRIGHT');
1204     };
1205 };
1206 </script>
1207 </body></html>