home · contact · privacy
66cb79584160760e95a2c4158503fbb53df8abc8
[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=8 value=24 />
8 terminal columns: <input id="n_cols" type="number" step=4 min=20 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 keys (see <a href="https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values">here</a> for non-obvious available values):<br />
14 move up (square grid): <input id="key_square_move_up" type="text" value="w" /> (hint: ArrowUp)<br />
15 move left (square grid): <input id="key_square_move_left" type="text" value="a" /> (hint: ArrowLeft)<br />
16 move down (square grid): <input id="key_square_move_down" type="text" value="s" /> (hint: ArrowDown)<br />
17 move right (square grid): <input id="key_square_move_right" type="text" value="d" /> (hint: ArrowRight)<br />
18 move up-left (hex grid): <input id="key_hex_move_upleft" type="text" value="w" /><br />
19 move up-right (hex grid): <input id="key_hex_move_upright" type="text" value="e" /><br />
20 move right (hex grid): <input id="key_hex_move_right" type="text" value="d" /><br />
21 move down-right (hex grid): <input id="key_hex_move_downright" type="text" value="x" /><br />
22 move down-left (hex grid): <input id="key_hex_move_downleft" type="text" value="y" /><br />
23 move left (hex grid): <input id="key_hex_move_left" type="text" value="a" /><br />
24 help: <input id="key_help" type="text" value="h" /><br />
25 flatten surroundings: <input id="key_flatten" type="text" value="F" /><br />
26 switch to chat mode: <input id="key_switch_to_chat" type="text" value="t" /><br />
27 switch to play mode: <input id="key_switch_to_play" type="text" value="p" /><br />
28 switch to study mode: <input id="key_switch_to_study" type="text" value="?" /><br />
29 edit tile (from play mode): <input id="key_switch_to_edit" type="text" value="m" /><br />
30 enter tile password (from play mode): <input id="key_switch_to_password" type="text" value="P" /><br />
31 annotate tile (from play mode): <input id="key_switch_to_annotate" type="text" value="M" /><br />
32 annotate portal (from play mode): <input id="key_switch_to_portal" type="text" value="T" /><br />
33 toggle terrain/control view (from study mode): <input id="key_toggle_map_mode" type="text" value="M" /><br />
34 </div>
35 <script>
36 "use strict";
37 let websocket_location = "wss://plomlompom.com/rogue_chat/";
38
39 let rows_selector = document.getElementById("n_rows");
40 let cols_selector = document.getElementById("n_cols");
41 let key_selectors = document.querySelectorAll('[id^="key_"]');
42
43 function restore_selector_value(selector) {
44     let stored_selection = window.localStorage.getItem(selector.id);
45     if (stored_selection) {
46         selector.value = stored_selection;
47     }
48 }
49 restore_selector_value(rows_selector);
50 restore_selector_value(cols_selector);
51 for (let key_selector of key_selectors) {
52     restore_selector_value(key_selector);
53 }
54
55 let terminal = {
56   foreground: 'white',
57   background: 'black',
58   initialize: function() {
59     this.rows = rows_selector.value;
60     this.cols = cols_selector.value;
61     this.pre_el = document.getElementById("terminal");
62     this.pre_el.style.color = this.foreground;
63     this.pre_el.style.backgroundColor = this.background;
64     this.content = [];
65       let line = []
66     for (let y = 0, x = 0; y <= this.rows; x++) {
67         if (x == this.cols) {
68             x = 0;
69             y += 1;
70             this.content.push(line);
71             line = [];
72             if (y == this.rows) {
73                 break;
74             }
75         }
76         line.push(' ');
77     }
78   },
79   blink_screen: function() {
80       this.pre_el.style.color = this.background;
81       this.pre_el.style.backgroundColor = this.foreground;
82       setTimeout(() => {
83           this.pre_el.style.color = this.foreground;
84           this.pre_el.style.backgroundColor = this.background;
85       }, 100);
86   },
87   refresh: function() {
88       let pre_string = '';
89       for (let y = 0; y < this.rows; y++) {
90           let line = this.content[y].join('');
91           pre_string += line + '\n';
92       }
93       this.pre_el.textContent = pre_string;
94   },
95   write: function(start_y, start_x, msg) {
96       for (let x = start_x, i = 0; x < this.cols && i < msg.length; x++, i++) {
97           this.content[start_y][x] = msg[i];
98       }
99   },
100   drawBox: function(start_y, start_x, height, width) {
101     let end_y = start_y + height;
102     let end_x = start_x + width;
103     for (let y = start_y, x = start_x; y < this.rows; x++) {
104       if (x == end_x) {
105         x = start_x;
106         y += 1;
107         if (y == end_y) {
108             break;
109         }
110       };
111       this.content[y][x] = ' ';
112     }
113   },
114 }
115 terminal.initialize();
116
117 let parser = {
118   tokenize: function(str) {
119     let token_ends = [];
120     let tokens = [];
121     let token = ''
122     let quoted = false;
123     let escaped = false;
124     for (let i = 0; i < str.length; i++) {
125       let c = str[i];
126       if (quoted) {
127         if (escaped) {
128           token += c;
129           escaped = false;
130         } else if (c == '\\') {
131           escaped = true;
132         } else if (c == '"') {
133           quoted = false
134         } else {
135           token += c;
136         }
137       } else if (c == '"') {
138         quoted = true
139       } else if (c === ' ') {
140         if (token.length > 0) {
141           token_ends.push(i);
142           tokens.push(token);
143           token = '';
144         }
145       } else {
146         token += c;
147       }
148     }
149     if (token.length > 0) {
150       tokens.push(token);
151     }
152     let token_starts = [];
153     for (let i = 0; i < token_ends.length; i++) {
154       token_starts.push(token_ends[i] - tokens[i].length);
155     };
156     return [tokens, token_starts];
157   },
158   parse_yx: function(position_string) {
159     let coordinate_strings = position_string.split(',')
160     let position = [0, 0];
161     position[0] = parseInt(coordinate_strings[0].slice(2));
162     position[1] = parseInt(coordinate_strings[1].slice(2));
163     return position;
164   },
165 }
166
167 class Thing {
168     constructor(yx) {
169         this.position = yx;
170     }
171 }
172
173 let server = {
174     init: function(url) {
175         this.url = url;
176         this.websocket = new WebSocket(this.url);
177         this.websocket.onopen = function(event) {
178             server.connected = true;
179             server.send(['TASKS']);
180             tui.log_msg("@ server connected! :)");
181             tui.switch_mode(mode_login);
182         };
183         this.websocket.onclose = function(event) {
184             server.connected = false;
185             tui.switch_mode(mode_waiting_for_server);
186             tui.log_msg("@ server disconnected :(");
187         };
188             this.websocket.onmessage = this.handle_event;
189         },
190     reconnect_to: function(url) {
191         this.websocket.close();
192         this.init(url);
193     },
194     send: function(tokens) {
195         this.websocket.send(unparser.untokenize(tokens));
196     },
197     handle_event: function(event) {
198         let tokens = parser.tokenize(event.data)[0];
199         if (tokens[0] === 'TURN') {
200             game.turn_complete = false;
201             game.things = {};
202             game.portals = {};
203             game.turn = parseInt(tokens[1]);
204         } else if (tokens[0] === 'THING_POS') {
205             game.get_thing(tokens[1], true).position = parser.parse_yx(tokens[2]);
206         } else if (tokens[0] === 'THING_NAME') {
207             game.get_thing(tokens[1], true).name_ = tokens[2];
208         } else if (tokens[0] === 'TASKS') {
209             game.tasks = tokens[1].split(',')
210         } else if (tokens[0] === 'MAP') {
211             game.map_geometry = tokens[1];
212             tui.init_keys();
213             game.map_size = parser.parse_yx(tokens[2]);
214             game.map = tokens[3]
215         } else if (tokens[0] === 'FOV') {
216             game.fov = tokens[1]
217         } else if (tokens[0] === 'MAP_CONTROL') {
218             game.map_control = tokens[1]
219         } else if (tokens[0] === 'GAME_STATE_COMPLETE') {
220             game.turn_complete = true;
221             explorer.empty_info_db();
222             if (tui.mode == mode_post_login_wait) {
223                 tui.switch_mode(mode_play);
224             } else if (tui.mode == mode_study) {
225                 explorer.query_info();
226             }
227             let t = game.get_thing(game.player_id);
228             if (t.position in game.portals) {
229                 tui.teleport_target = game.portals[t.position];
230                 tui.switch_mode(mode_teleport);
231                 return;
232             }
233             tui.full_refresh();
234         } else if (tokens[0] === 'CHAT') {
235              tui.log_msg('# ' + tokens[1], 1);
236         } else if (tokens[0] === 'PLAYER_ID') {
237             game.player_id = parseInt(tokens[1]);
238         } else if (tokens[0] === 'LOGIN_OK') {
239             this.send(['GET_GAMESTATE']);
240             tui.switch_mode(mode_post_login_wait);
241         } else if (tokens[0] === 'PORTAL') {
242             let position = parser.parse_yx(tokens[1]);
243             game.portals[position] = tokens[2];
244         } else if (tokens[0] === 'ANNOTATION') {
245             let position = parser.parse_yx(tokens[1]);
246             explorer.update_info_db(position, tokens[2]);
247         } else if (tokens[0] === 'UNHANDLED_INPUT') {
248             tui.log_msg('? unknown command');
249         } else if (tokens[0] === 'PLAY_ERROR') {
250             terminal.blink_screen();
251         } else if (tokens[0] === 'ARGUMENT_ERROR') {
252             tui.log_msg('? syntax error: ' + tokens[1]);
253         } else if (tokens[0] === 'GAME_ERROR') {
254             tui.log_msg('? game error: ' + tokens[1]);
255         } else if (tokens[0] === 'PONG') {
256             ;
257         } else {
258             tui.log_msg('? unhandled input: ' + event.data);
259         }
260     }
261 }
262
263 let unparser = {
264     quote: function(str) {
265         let quoted = ['"'];
266         for (let i = 0; i < str.length; i++) {
267             let c = str[i];
268             if (['"', '\\'].includes(c)) {
269                 quoted.push('\\');
270             };
271             quoted.push(c);
272         }
273         quoted.push('"');
274         return quoted.join('');
275     },
276     to_yx: function(yx_coordinate) {
277         return "Y:" + yx_coordinate[0] + ",X:" + yx_coordinate[1];
278     },
279     untokenize: function(tokens) {
280         let quoted_tokens = [];
281         for (let token of tokens) {
282             quoted_tokens.push(this.quote(token));
283         }
284         return quoted_tokens.join(" ");
285     }
286 }
287
288 class Mode {
289     constructor(name, help_intro, has_input_prompt=false, shows_info=false, is_intro=false) {
290         this.name = name;
291         this.has_input_prompt = has_input_prompt;
292         this.shows_info= shows_info;
293         this.is_intro = is_intro;
294         this.help_intro = help_intro;
295     }
296 }
297 let mode_waiting_for_server = new Mode('waiting_for_server', 'Waiting for a server response.', false, false, true);
298 let mode_login = new Mode('login', 'Pick your player name.', true, false, true);
299 let mode_post_login_wait = new Mode('waiting for game world', 'Waiting for a server response.', false, false, true);
300 let mode_chat = new Mode('chat', '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 to all users.  Lines that start with a "/" are used for commands like:', true, false);
301   let mode_annotate = new Mode('annotate', '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.', true, true);
302 let mode_play = new Mode('play', 'This mode allows you to interact with the map.', false, false);
303 let mode_study = new Mode('study', '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.', false, true);
304 let mode_edit = new Mode('edit', '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.', false, false);
305 let mode_teleport = new Mode('teleport', 'Follow the instructions to re-connect and log-in to another server, or enter anything else to abort.', true);
306 let mode_portal = new Mode('portal', '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.', true, true);
307 let mode_password = new Mode('password', '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.', true, false, false);
308
309 let tui = {
310   mode: mode_waiting_for_server,
311   log: [],
312   input_prompt: '> ',
313   input_lines: [],
314   window_width: terminal.cols / 2,
315   height_turn_line: 1,
316   height_mode_line: 1,
317   height_input: 1,
318   password: 'foo',
319   show_help: false,
320   init: function() {
321       this.inputEl = document.getElementById("input");
322       this.inputEl.focus();
323       this.recalc_input_lines();
324       this.height_header = this.height_turn_line + this.height_mode_line;
325       this.log_msg("@ waiting for server connection ...");
326       this.init_keys();
327   },
328   init_keys: function() {
329     this.keys = {};
330     for (let key_selector of key_selectors) {
331         this.keys[key_selector.id.slice(4)] = key_selector.value;
332     }
333     this.movement_keys = {
334         [this.keys.square_move_up]: 'UP',
335         [this.keys.square_move_left]: 'LEFT',
336         [this.keys.square_move_down]: 'DOWN',
337         [this.keys.square_move_right]: 'RIGHT'
338     };
339     if (game.map_geometry == 'Hex') {
340         this.movement_keys = {
341             [this.keys.hex_move_upleft]: 'UPLEFT',
342             [this.keys.hex_move_upright]: 'UPRIGHT',
343             [this.keys.hex_move_right]: 'RIGHT',
344             [this.keys.hex_move_downright]: 'DOWNRIGHT',
345             [this.keys.hex_move_downleft]: 'DOWNLEFT',
346             [this.keys.hex_move_left]: 'LEFT'
347         };
348     };
349   },
350   switch_mode: function(mode) {
351     this.show_help = false;
352     this.map_mode = 'terrain';
353     if (mode.shows_info && game.player_id in game.things) {
354       explorer.position = game.things[game.player_id].position;
355     }
356     this.mode = mode;
357     this.empty_input();
358     this.restore_input_values();
359     if (mode == mode_login) {
360         if (this.login_name) {
361             server.send(['LOGIN', this.login_name]);
362         } else {
363             this.log_msg("? need login name");
364         }
365     } else if (mode == mode_edit) {
366         this.show_help = true;
367     } else if (mode == mode_teleport) {
368         tui.log_msg("@ May teleport to: " + tui.teleport_target);
369         tui.log_msg("@ Enter 'YES!' to entusiastically affirm.");
370     }
371     this.full_refresh();
372   },
373   restore_input_values: function() {
374       if (this.mode == mode_annotate && explorer.position in explorer.info_db) {
375           let info = explorer.info_db[explorer.position];
376           if (info != "(none)") {
377               this.inputEl.value = info;
378               this.recalc_input_lines();
379           }
380       } else if (this.mode == mode_portal && explorer.position in game.portals) {
381           let portal = game.portals[explorer.position]
382           this.inputEl.value = portal;
383           this.recalc_input_lines();
384       } else if (this.mode == mode_password) {
385           this.inputEl.value = this.password;
386           this.recalc_input_lines();
387       }
388   },
389   empty_input: function(str) {
390       this.inputEl.value = "";
391       if (this.mode.has_input_prompt) {
392           this.recalc_input_lines();
393       } else {
394           this.height_input = 0;
395       }
396   },
397   recalc_input_lines: function() {
398       this.input_lines = this.msg_into_lines_of_width(this.input_prompt + this.inputEl.value, this.window_width);
399       this.height_input = this.input_lines.length;
400   },
401   msg_into_lines_of_width: function(msg, width) {
402     let chunk = "";
403     let lines = [];
404     for (let i = 0, x = 0; i < msg.length; i++, x++) {
405       if (x >= width || msg[i] == "\n") {
406         lines.push(chunk);
407         chunk = "";
408         x = 0;
409       };
410       if (msg[i] != "\n") {
411         chunk += msg[i];
412       }
413     }
414     lines.push(chunk);
415     return lines;
416   },
417   log_msg: function(msg) {
418       this.log.push(msg);
419       while (this.log.length > 100) {
420         this.log.shift();
421       };
422       this.full_refresh();
423   },
424   draw_map: function() {
425     let map_lines_split = [];
426     let line = [];
427     let map_content = game.map;
428     if (this.map_mode == 'control') {
429         map_content = game.map_control;
430     }
431     for (let i = 0, j = 0; i < game.map.length; i++, j++) {
432         if (j == game.map_size[1]) {
433             map_lines_split.push(line);
434             line = [];
435             j = 0;
436         };
437         line.push(map_content[i]);
438     };
439     map_lines_split.push(line);
440     if (this.map_mode == 'terrain') {
441         for (const thing_id in game.things) {
442             let t = game.things[thing_id];
443             map_lines_split[t.position[0]][t.position[1]] = '@';
444         };
445     }
446     if (tui.mode.shows_info) {
447         map_lines_split[explorer.position[0]][explorer.position[1]] = '?';
448     }
449     let map_lines = []
450     if (game.map_geometry == 'Square') {
451         for (let line_split of map_lines_split) {
452             map_lines.push(line_split.join(' '));
453         };
454     } else if (game.map_geometry == 'Hex') {
455         let indent = 0
456         for (let line_split of map_lines_split) {
457             map_lines.push(' '.repeat(indent) + line_split.join(' '));
458             if (indent == 0) {
459                 indent = 1;
460             } else {
461                 indent = 0;
462             };
463         };
464     }
465     let window_center = [terminal.rows / 2, this.window_width / 2];
466     let player = game.things[game.player_id];
467     let center_position = [player.position[0], player.position[1]];
468     if (tui.mode.shows_info) {
469         center_position = [explorer.position[0], explorer.position[1]];
470     }
471     center_position[1] = center_position[1] * 2;
472     let offset = [center_position[0] - window_center[0],
473                   center_position[1] - window_center[1]]
474     if (game.map_geometry == 'Hex' && offset[0] % 2) {
475         offset[1] += 1;
476     };
477     let term_y = Math.max(0, -offset[0]);
478     let term_x = Math.max(0, -offset[1]);
479     let map_y = Math.max(0, offset[0]);
480     let map_x = Math.max(0, offset[1]);
481     for (; term_y < terminal.rows && map_y < game.map_size[0]; term_y++, map_y++) {
482         let to_draw = map_lines[map_y].slice(map_x, this.window_width + offset[1]);
483         terminal.write(term_y, term_x, to_draw);
484     }
485   },
486   draw_mode_line: function() {
487       let help = 'hit [' + this.keys.help + '] for help';
488       if (this.mode.has_input_prompt) {
489           help = 'enter /help for help';
490       }
491       terminal.write(0, this.window_width, 'MODE: ' + this.mode.name + ' – ' + help);
492   },
493   draw_turn_line: function(n) {
494     terminal.write(1, this.window_width, 'TURN: ' + game.turn);
495   },
496   draw_history: function() {
497       let log_display_lines = [];
498       for (let line of this.log) {
499           log_display_lines = log_display_lines.concat(this.msg_into_lines_of_width(line, this.window_width));
500       };
501       for (let y = terminal.rows - 1 - this.height_input,
502                i = log_display_lines.length - 1;
503            y >= this.height_header && i >= 0;
504            y--, i--) {
505           terminal.write(y, this.window_width, log_display_lines[i]);
506       }
507   },
508   draw_info: function() {
509     let lines = this.msg_into_lines_of_width(explorer.get_info(), this.window_width);
510     for (let y = this.height_header, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
511       terminal.write(y, this.window_width, lines[i]);
512     }
513   },
514   draw_input: function() {
515     if (this.mode.has_input_prompt) {
516         for (let y = terminal.rows - this.height_input, i = 0; i < this.input_lines.length; y++, i++) {
517             terminal.write(y, this.window_width, this.input_lines[i]);
518         }
519     }
520   },
521   draw_help: function() {
522       let movement_keys_desc = Object.keys(this.movement_keys).join(',');
523       let content = this.mode.name + " mode help\n\n" + this.mode.help_intro + "\n\n";
524       if (this.mode == mode_play) {
525           content += "Available actions:\n";
526           if (game.tasks.includes('MOVE')) {
527               content += "[" + movement_keys_desc + "] – move player\n";
528           }
529           if (game.tasks.includes('FLATTEN_SURROUNDINGS')) {
530               content += "[" + tui.keys.flatten + "] – flatten player's surroundings\n";
531           }
532           content += '\nOther modes available from here:\n';
533           content += '[' + this.keys.switch_to_chat + '] – chat mode\n';
534           content += '[' + this.keys.switch_to_study + '] – study mode\n';
535           content += '[' + this.keys.switch_to_edit + '] – terrain edit mode\n';
536           content += '[' + this.keys.switch_to_portal + '] – portal edit mode\n';
537           content += '[' + this.keys.switch_to_annotate + '] – annotation mode\n';
538           content += '[' + this.keys.switch_to_password + '] – password input mode\n';
539       } else if (this.mode == mode_study) {
540           content += "Available actions:\n";
541           content += '[' + movement_keys_desc + '] – move question mark\n';
542           content += '[' + this.keys.toggle_map_mode + '] – toggle view between terrain, and password protection areas\n';
543           content += '\nOther modes available from here:\n';
544           content += '[' + this.keys.switch_to_chat + '] – chat mode\n';
545           content += '[' + this.keys.switch_to_play + '] – play mode\n';
546       } else if (this.mode == mode_chat) {
547           content += '/nick NAME – re-name yourself to NAME\n';
548           content += '/msg USER TEXT – send TEXT to USER\n';
549           content += '/' + this.keys.switch_to_play + ' or /play – switch to play mode\n';
550           content += '/' + this.keys.switch_to_study + ' or /study – switch to study mode\n';
551       }
552       let start_x = 0;
553       if (!this.mode.has_input_prompt) {
554           start_x = this.window_width
555       }
556       terminal.drawBox(0, start_x, terminal.rows, this.window_width);
557       let lines = this.msg_into_lines_of_width(content, this.window_width);
558       for (let y = 0, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
559           terminal.write(y, start_x, lines[i]);
560       }
561   },
562   full_refresh: function() {
563     terminal.drawBox(0, 0, terminal.rows, terminal.cols);
564     if (this.mode.is_intro) {
565         this.draw_history();
566         this.draw_input();
567     } else {
568         if (game.turn_complete) {
569             this.draw_map();
570             this.draw_turn_line();
571         }
572         this.draw_mode_line();
573         if (this.mode.shows_info) {
574           this.draw_info();
575         } else {
576           this.draw_history();
577         }
578         this.draw_input();
579     }
580     if (this.show_help) {
581         this.draw_help();
582     }
583     terminal.refresh();
584   }
585 }
586
587 let game = {
588     init: function() {
589         this.things = {};
590         this.turn = -1;
591         this.map = "";
592         this.map_control = "";
593         this.map_size = [0,0];
594         this.player_id = -1;
595         this.portals = {};
596         this.tasks = {};
597     },
598     get_thing: function(id_, create_if_not_found=false) {
599         if (id_ in game.things) {
600             return game.things[id_];
601         } else if (create_if_not_found) {
602             let t = new Thing([0,0]);
603             game.things[id_] = t;
604             return t;
605         };
606     },
607     move: function(start_position, direction) {
608         let target = [start_position[0], start_position[1]];
609         if (direction == 'LEFT') {
610             target[1] -= 1;
611         } else if (direction == 'RIGHT') {
612             target[1] += 1;
613         } else if (game.map_geometry == 'Square') {
614             if (direction == 'UP') {
615                 target[0] -= 1;
616             } else if (direction == 'DOWN') {
617                 target[0] += 1;
618             };
619         } else if (game.map_geometry == 'Hex') {
620             let start_indented = start_position[0] % 2;
621             if (direction == 'UPLEFT') {
622                 target[0] -= 1;
623                 if (!start_indented) {
624                     target[1] -= 1;
625                 }
626             } else if (direction == 'UPRIGHT') {
627                 target[0] -= 1;
628                 if (start_indented) {
629                     target[1] += 1;
630                 }
631             } else if (direction == 'DOWNLEFT') {
632                 target[0] += 1;
633                 if (!start_indented) {
634                     target[1] -= 1;
635                 }
636             } else if (direction == 'DOWNRIGHT') {
637                 target[0] += 1;
638                 if (start_indented) {
639                     target[1] += 1;
640                 }
641             };
642         };
643         if (target[0] < 0 || target[1] < 0 ||
644             target[0] >= this.map_size[0] || target[1] >= this.map_size[1]) {
645             return null;
646         };
647         return target;
648     }
649 }
650
651 game.init();
652 tui.init();
653 tui.full_refresh();
654 server.init(websocket_location);
655
656 let explorer = {
657     position: [0,0],
658     info_db: {},
659     move: function(direction) {
660         let target = game.move(this.position, direction);
661         if (target) {
662             this.position = target
663             this.query_info();
664         } else {
665             terminal.blink_screen();
666         };
667     },
668     update_info_db: function(yx, str) {
669         this.info_db[yx] = str;
670         if (tui.mode == mode_study) {
671             tui.full_refresh();
672         }
673     },
674     empty_info_db: function() {
675         this.info_db = {};
676         if (tui.mode == mode_study) {
677             tui.full_refresh();
678         }
679     },
680     query_info: function() {
681         server.send(["GET_ANNOTATION", unparser.to_yx(explorer.position)]);
682     },
683     get_info: function() {
684         let position_i = this.position[0] * game.map_size[1] + this.position[1];
685         if (game.fov[position_i] != '.') {
686             return 'outside field of view';
687         };
688         let info = "";
689         info += "TERRAIN: " + game.map[position_i] + "\n";
690         for (let t_id in game.things) {
691              let t = game.things[t_id];
692              if (t.position[0] == this.position[0] && t.position[1] == this.position[1]) {
693                  info += "PLAYER @";
694                  if (t.name_) {
695                      info += ": " + t.name_;
696                  }
697                  info += "\n";
698              }
699         }
700         if (this.position in game.portals) {
701             info += "PORTAL: " + game.portals[this.position] + "\n";
702         }
703         if (this.position in this.info_db) {
704             info += "ANNOTATIONS: " + this.info_db[this.position];
705         } else {
706             info += 'waiting …';
707         }
708         return info;
709     },
710     annotate: function(msg) {
711         if (msg.length == 0) {
712             msg = " ";  // triggers annotation deletion
713         }
714         server.send(["ANNOTATE", unparser.to_yx(explorer.position), msg, tui.password]);
715     },
716     set_portal: function(msg) {
717         if (msg.length == 0) {
718             msg = " ";  // triggers portal deletion
719         }
720         server.send(["PORTAL", unparser.to_yx(explorer.position), msg, tui.password]);
721     }
722 }
723
724 tui.inputEl.addEventListener('input', (event) => {
725     if (tui.mode.has_input_prompt) {
726         let max_length = tui.window_width * terminal.rows - tui.input_prompt.length;
727         if (tui.inputEl.value.length > max_length) {
728             tui.inputEl.value = tui.inputEl.value.slice(0, max_length);
729         };
730         tui.recalc_input_lines();
731     } else if (tui.mode == mode_edit && tui.inputEl.value.length > 0) {
732         server.send(["TASK:WRITE", tui.inputEl.value[0], tui.password]);
733         tui.switch_mode(mode_play);
734     }
735     tui.full_refresh();
736 }, false);
737 tui.inputEl.addEventListener('keydown', (event) => {
738     tui.show_help = false;
739     if (event.key == 'Enter') {
740         event.preventDefault();
741     }
742     if (tui.mode.has_input_prompt && event.key == 'Enter' && tui.inputEl.value == '/help') {
743         tui.show_help = true;
744         tui.empty_input();
745         tui.restore_input_values();
746     } else if (!tui.mode.has_input_prompt && event.key == tui.keys.help) {
747         tui.show_help = true;
748     } else if (tui.mode == mode_login && event.key == 'Enter') {
749         tui.login_name = tui.inputEl.value;
750         server.send(['LOGIN', tui.inputEl.value]);
751         tui.empty_input();
752     } else if (tui.mode == mode_portal && event.key == 'Enter') {
753         explorer.set_portal(tui.inputEl.value);
754         tui.switch_mode(mode_play);
755     } else if (tui.mode == mode_annotate && event.key == 'Enter') {
756         explorer.annotate(tui.inputEl.value);
757         tui.switch_mode(mode_play);
758     } else if (tui.mode == mode_password && event.key == 'Enter') {
759         if (tui.inputEl.value.length == 0) {
760             tui.inputEl.value = " ";
761         }
762         tui.password = tui.inputEl.value
763         tui.switch_mode(mode_play);
764     } else if (tui.mode == mode_teleport && event.key == 'Enter') {
765         if (tui.inputEl.value == 'YES!') {
766             server.reconnect_to(tui.teleport_target);
767         } else {
768             tui.log_msg('@ teleport aborted');
769             tui.switch_mode(mode_play);
770         };
771     } else if (tui.mode == mode_chat && event.key == 'Enter') {
772         let [tokens, token_starts] = parser.tokenize(tui.inputEl.value);
773         if (tokens.length > 0 && tokens[0].length > 0) {
774             if (tui.inputEl.value[0][0] == '/') {
775                 if (tokens[0].slice(1) == 'play' || tokens[0][1] == tui.keys.switch_to_play) {
776                     tui.switch_mode(mode_play);
777                 } else if (tokens[0].slice(1) == 'study' || tokens[0][1] == tui.keys.switch_to_study) {
778                     tui.switch_mode(mode_study);
779                 } else if (tokens[0].slice(1) == 'nick') {
780                     if (tokens.length > 1) {
781                         server.send(['NICK', tokens[1]]);
782                     } else {
783                         tui.log_msg('? need new name');
784                     }
785                 } else if (tokens[0].slice(1) == 'msg') {
786                     if (tokens.length > 2) {
787                         let msg = tui.inputEl.value.slice(token_starts[2]);
788                         server.send(['QUERY', tokens[1], msg]);
789                     } else {
790                         tui.log_msg('? need message target and message');
791                     }
792                 } else {
793                     tui.log_msg('? unknown command');
794                 }
795             } else {
796                     server.send(['ALL', tui.inputEl.value]);
797             }
798         } else if (tui.inputEl.valuelength > 0) {
799                 server.send(['ALL', tui.inputEl.value]);
800         }
801         tui.empty_input();
802     } else if (tui.mode == mode_play) {
803           if (event.key === tui.keys.switch_to_chat) {
804               event.preventDefault();
805               tui.switch_mode(mode_chat);
806           } else if (event.key === tui.keys.switch_to_edit
807                      && game.tasks.includes('WRITE')) {
808               event.preventDefault();
809               tui.switch_mode(mode_edit);
810           } else if (event.key === tui.keys.switch_to_study) {
811               tui.switch_mode(mode_study);
812           } else if (event.key === tui.keys.switch_to_password) {
813               event.preventDefault();
814               tui.switch_mode(mode_password);
815           } else if (event.key === tui.keys.flatten
816                      && game.tasks.includes('FLATTEN_SURROUNDINGS')) {
817               server.send(["TASK:FLATTEN_SURROUNDINGS", tui.password]);
818           } else if (event.key in tui.movement_keys
819                      && game.tasks.includes('MOVE')) {
820               server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
821           } else if (event.key === tui.keys.switch_to_portal) {
822               event.preventDefault();
823               tui.switch_mode(mode_portal);
824           } else if (event.key === tui.keys.switch_to_annotate) {
825               event.preventDefault();
826               tui.switch_mode(mode_annotate);
827           };
828     } else if (tui.mode == mode_study) {
829         if (event.key === tui.keys.switch_to_chat) {
830             event.preventDefault();
831             tui.switch_mode(mode_chat);
832         } else if (event.key == tui.keys.switch_to_play) {
833             tui.switch_mode(mode_play);
834         } else if (event.key in tui.movement_keys) {
835             explorer.move(tui.movement_keys[event.key]);
836         } else if (event.key == tui.keys.toggle_map_mode) {
837             if (tui.map_mode == 'terrain') {
838                 tui.map_mode = 'control';
839             } else {
840                 tui.map_mode = 'terrain';
841             }
842         };
843     }
844     tui.full_refresh();
845 }, false);
846
847 rows_selector.addEventListener('input', function() {
848     if (rows_selector.value % 4 != 0) {
849         return;
850     }
851     window.localStorage.setItem(rows_selector.id, rows_selector.value);
852     terminal.initialize();
853     tui.full_refresh();
854 }, false);
855 cols_selector.addEventListener('input', function() {
856     if (cols_selector.value % 4 != 0) {
857         return;
858     }
859     window.localStorage.setItem(cols_selector.id, cols_selector.value);
860     terminal.initialize();
861     tui.window_width = terminal.cols / 2,
862     tui.full_refresh();
863 }, false);
864 for (let key_selector of key_selectors) {
865     key_selector.addEventListener('input', function() {
866         window.localStorage.setItem(key_selector.id, key_selector.value);
867         tui.init_keys();
868     }, false);
869 }
870 window.setInterval(function() {
871     if (!(['input', 'n_cols', 'n_rows'].includes(document.activeElement.id)
872           || document.activeElement.id.startsWith('key_'))) {
873         tui.inputEl.focus();
874     }
875 }, 100);
876 window.setInterval(function() {
877     if (server.connected) {
878         server.send(['PING']);
879     } else {
880         server.reconnect_to(server.url);
881         tui.log_msg('@ attempting reconnect …')
882     }
883 }, 5000);
884 </script>
885 </body></html>