home · contact · privacy
Put all editing below play mode.
[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             window.setInterval(function() { server.send(['PING']) }, 30000);
179             this.send('TASKS');
180             tui.log_msg("@ server connected! :)");
181             tui.switch_mode(mode_login);
182         };
183         this.websocket.onclose = function(event) {
184             tui.log_msg("@ server disconnected :(");
185             tui.log_msg("@ hint: try the '/reconnect' command");
186         };
187             this.websocket.onmessage = this.handle_event;
188         },
189     reconnect: function() {
190            this.reconnect_to(this.url);
191     },
192     reconnect_to: function(url) {
193         this.websocket.close();
194         this.init(url);
195     },
196     send: function(tokens) {
197         this.websocket.send(unparser.untokenize(tokens));
198     },
199     handle_event: function(event) {
200         let tokens = parser.tokenize(event.data)[0];
201         if (tokens[0] === 'TURN') {
202             game.turn_complete = false;
203             game.things = {};
204             game.portals = {};
205             game.turn = parseInt(tokens[1]);
206         } else if (tokens[0] === 'THING_POS') {
207             game.get_thing(tokens[1], true).position = parser.parse_yx(tokens[2]);
208         } else if (tokens[0] === 'THING_NAME') {
209             game.get_thing(tokens[1], true).name_ = tokens[2];
210         } else if (tokens[0] === 'TASKS') {
211             game.tasks = tokens[1].split(',')
212         } else if (tokens[0] === 'MAP') {
213             game.map_geometry = tokens[1];
214             tui.init_keys();
215             game.map_size = parser.parse_yx(tokens[2]);
216             game.map = tokens[3]
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             console.log('PONG');
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 (hit any key to disappear)\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             tui.full_refresh();
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 info = "";
686         let position_i = this.position[0] * game.map_size[1] + this.position[1];
687         info += "TERRAIN: " + game.map[position_i] + "\n";
688         for (let t_id in game.things) {
689              let t = game.things[t_id];
690              if (t.position[0] == this.position[0] && t.position[1] == this.position[1]) {
691                  info += "PLAYER @";
692                  if (t.name_) {
693                      info += ": " + t.name_;
694                  }
695                  info += "\n";
696              }
697         }
698         if (this.position in game.portals) {
699             info += "PORTAL: " + game.portals[this.position] + "\n";
700         }
701         if (this.position in this.info_db) {
702             info += "ANNOTATIONS: " + this.info_db[this.position];
703         } else {
704             info += 'waiting …';
705         }
706         return info;
707     },
708     annotate: function(msg) {
709         if (msg.length == 0) {
710             msg = " ";  // triggers annotation deletion
711         }
712         server.send(["ANNOTATE", unparser.to_yx(explorer.position), msg, tui.password]);
713     },
714     set_portal: function(msg) {
715         if (msg.length == 0) {
716             msg = " ";  // triggers portal deletion
717         }
718         server.send(["PORTAL", unparser.to_yx(explorer.position), msg, tui.password]);
719     }
720 }
721
722 tui.inputEl.addEventListener('input', (event) => {
723     if (tui.mode.has_input_prompt) {
724         let max_length = tui.window_width * terminal.rows - tui.input_prompt.length;
725         if (tui.inputEl.value.length > max_length) {
726             tui.inputEl.value = tui.inputEl.value.slice(0, max_length);
727         };
728         tui.recalc_input_lines();
729         tui.full_refresh();
730     } else if (tui.mode == mode_edit && tui.inputEl.value.length > 0) {
731         server.send(["TASK:WRITE", tui.inputEl.value[0], tui.password]);
732         tui.switch_mode(mode_play);
733     } else if (tui.mode == mode_teleport) {
734         if (['Y', 'y'].includes(tui.inputEl.value[0])) {
735             server.reconnect_to(tui.teleport_target);
736         } else {
737             tui.log_msg("@ teleportation aborted");
738             tui.switch_mode(mode_play);
739         }
740     }
741 }, false);
742 tui.inputEl.addEventListener('keydown', (event) => {
743     tui.show_help = false;
744     if (event.key == 'Enter') {
745         event.preventDefault();
746     }
747     if (tui.mode.has_input_prompt && event.key == 'Enter' && tui.inputEl.value == '/help') {
748         tui.show_help = true;
749         tui.empty_input();
750         tui.restore_input_values();
751         tui.full_refresh();
752     } else if (!tui.mode.has_input_prompt && event.key == tui.keys.help) {
753         tui.show_help = true;
754         tui.full_refresh();
755     } else if (tui.mode == mode_login && event.key == 'Enter') {
756         tui.login_name = tui.inputEl.value;
757         server.send(['LOGIN', tui.inputEl.value]);
758         tui.empty_input();
759     } else if (tui.mode == mode_portal && event.key == 'Enter') {
760         explorer.set_portal(tui.inputEl.value);
761         tui.switch_mode(mode_play);
762     } else if (tui.mode == mode_annotate && event.key == 'Enter') {
763         explorer.annotate(tui.inputEl.value);
764         tui.switch_mode(mode_play);
765     } else if (tui.mode == mode_password && event.key == 'Enter') {
766         if (tui.inputEl.value.length == 0) {
767             tui.inputEl.value = " ";
768         }
769         tui.password = tui.inputEl.value
770         tui.switch_mode(mode_play);
771     } else if (tui.mode == mode_teleport && event.key == 'Enter') {
772         if (tui.inputEl.value == 'YES!') {
773             server.reconnect_to(tui.teleport_target);
774         } else {
775             tui.log_msg('@ teleport aborted');
776             tui.switch_mode(mode_play);
777         };
778     } else if (tui.mode == mode_chat && event.key == 'Enter') {
779         let [tokens, token_starts] = parser.tokenize(tui.inputEl.value);
780         if (tokens.length > 0 && tokens[0].length > 0) {
781             if (tui.inputEl.value[0][0] == '/') {
782                 if (tokens[0].slice(1) == 'play' || tokens[0][1] == tui.keys.switch_to_play) {
783                     tui.switch_mode(mode_play);
784                 } else if (tokens[0].slice(1) == 'study' || tokens[0][1] == tui.keys.switch_to_study) {
785                     tui.switch_mode(mode_study);
786                 } else if (tokens[0].slice(1) == 'nick') {
787                     if (tokens.length > 1) {
788                         server.send(['NICK', tokens[1]]);
789                     } else {
790                         tui.log_msg('? need new name');
791                     }
792                 } else if (tokens[0].slice(1) == 'msg') {
793                     if (tokens.length > 2) {
794                         let msg = tui.inputEl.value.slice(token_starts[2]);
795                         server.send(['QUERY', tokens[1], msg]);
796                     } else {
797                         tui.log_msg('? need message target and message');
798                     }
799                 } else if (tokens[0].slice(1) == 'reconnect') {
800                     if (tokens.length > 1) {
801                 server.reconnect_to(tokens[1]);
802                     } else {
803                         server.reconnect();
804                     }
805                 } else {
806                     tui.log_msg('? unknown command');
807                 }
808             } else {
809                     server.send(['ALL', tui.inputEl.value]);
810             }
811         } else if (tui.inputEl.valuelength > 0) {
812                 server.send(['ALL', tui.inputEl.value]);
813         }
814         tui.empty_input();
815         tui.full_refresh();
816     } else if (tui.mode == mode_play) {
817           if (event.key === tui.keys.switch_to_chat) {
818               event.preventDefault();
819               tui.switch_mode(mode_chat);
820           } else if (event.key === tui.keys.switch_to_edit
821                      && game.tasks.includes('WRITE')) {
822               event.preventDefault();
823               tui.switch_mode(mode_edit);
824           } else if (event.key === tui.keys.switch_to_study) {
825               tui.switch_mode(mode_study);
826           } else if (event.key === tui.keys.switch_to_password) {
827               event.preventDefault();
828               tui.switch_mode(mode_password);
829           } else if (event.key === tui.keys.flatten
830                      && game.tasks.includes('FLATTEN_SURROUNDINGS')) {
831               server.send(["TASK:FLATTEN_SURROUNDINGS", tui.password]);
832           } else if (event.key in tui.movement_keys
833                      && game.tasks.includes('MOVE')) {
834               server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
835           } else if (event.key === tui.keys.switch_to_portal) {
836               event.preventDefault();
837               tui.switch_mode(mode_portal);
838           } else if (event.key === tui.keys.switch_to_annotate) {
839               event.preventDefault();
840               tui.switch_mode(mode_annotate);
841           };
842     } else if (tui.mode == mode_study) {
843         if (event.key === tui.keys.switch_to_chat) {
844             event.preventDefault();
845             tui.switch_mode(mode_chat);
846         } else if (event.key == tui.keys.switch_to_play) {
847             tui.switch_mode(mode_play);
848         } else if (event.key in tui.movement_keys) {
849             explorer.move(tui.movement_keys[event.key]);
850         } else if (event.key == tui.keys.toggle_map_mode) {
851             if (tui.map_mode == 'terrain') {
852                 tui.map_mode = 'control';
853             } else {
854                 tui.map_mode = 'terrain';
855             }
856             tui.full_refresh();
857         };
858     }
859 }, false);
860
861 rows_selector.addEventListener('input', function() {
862     if (rows_selector.value % 4 != 0) {
863         return;
864     }
865     window.localStorage.setItem(rows_selector.id, rows_selector.value);
866     terminal.initialize();
867     tui.full_refresh();
868 }, false);
869 cols_selector.addEventListener('input', function() {
870     if (cols_selector.value % 4 != 0) {
871         return;
872     }
873     window.localStorage.setItem(cols_selector.id, cols_selector.value);
874     terminal.initialize();
875     tui.window_width = terminal.cols / 2,
876     tui.full_refresh();
877 }, false);
878 for (let key_selector of key_selectors) {
879     key_selector.addEventListener('input', function() {
880         window.localStorage.setItem(key_selector.id, key_selector.value);
881         tui.init_keys();
882     }, false);
883 }
884 window.setInterval(function() {
885     if (!(['input', 'n_cols', 'n_rows'].includes(document.activeElement.id)
886           || document.activeElement.id.startsWith('key_'))) {
887         tui.inputEl.focus();
888     }
889 }, 100);
890 </script>
891 </body></html>