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