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