home · contact · privacy
Improve help system.
[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 terrain (from play mode): <input id="key_switch_to_edit" type="text" value="m" /><br />
30 enter terrain password (from play mode): <input id="key_switch_to_password" type="text" value="P" /><br />
31 annotate terrain (from study mode): <input id="key_switch_to_annotate" type="text" value="m" /><br />
32 annotate portal (from study mode): <input id="key_switch_to_portal" type="text" value="P" /><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.  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 (unless obscured by this help screen here, which you can disappear with any key).', false, true);
304 let mode_edit = new Mode('edit', 'This mode allows you to change the map tile you currently stand on (if your terrain 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 imprints/edits/removes a teleportation target on the ground you are currently standing on.  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 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, keep_pos=false) {
351     this.show_help = false;
352     this.map_mode = 'terrain';
353     if (mode == mode_study && !keep_pos && 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     if (mode == mode_annotate && explorer.position in explorer.info_db) {
359         let info = explorer.info_db[explorer.position];
360         if (info != "(none)") {
361             this.inputEl.value = info;
362             this.recalc_input_lines();
363         }
364     } else if (mode == mode_login) {
365         if (this.login_name) {
366             server.send(['LOGIN', this.login_name]);
367         } else {
368             this.log_msg("? need login name");
369         }
370     } else if (mode == mode_edit) {
371         this.show_help = true;
372     } else if (mode == mode_portal && explorer.position in game.portals) {
373         let portal = game.portals[explorer.position]
374         this.inputEl.value = portal;
375         this.recalc_input_lines();
376     } else if (mode == mode_password) {
377         this.inputEl.value = this.password;
378         this.recalc_input_lines();
379     } else if (mode == mode_teleport) {
380         tui.log_msg("@ May teleport to: " + tui.teleport_target);
381         tui.log_msg("@ Enter 'YES!' to entusiastically affirm.");
382     }
383     this.full_refresh();
384   },
385   empty_input: function(str) {
386       this.inputEl.value = "";
387       if (this.mode.has_input_prompt) {
388           this.recalc_input_lines();
389       } else {
390           this.height_input = 0;
391       }
392   },
393   recalc_input_lines: function() {
394       this.input_lines = this.msg_into_lines_of_width(this.input_prompt + this.inputEl.value, this.window_width);
395       this.height_input = this.input_lines.length;
396   },
397   msg_into_lines_of_width: function(msg, width) {
398     let chunk = "";
399     let lines = [];
400     for (let i = 0, x = 0; i < msg.length; i++, x++) {
401       if (x >= width || msg[i] == "\n") {
402         lines.push(chunk);
403         chunk = "";
404         x = 0;
405       };
406       if (msg[i] != "\n") {
407         chunk += msg[i];
408       }
409     }
410     lines.push(chunk);
411     return lines;
412   },
413   log_msg: function(msg) {
414       this.log.push(msg);
415       while (this.log.length > 100) {
416         this.log.shift();
417       };
418       this.full_refresh();
419   },
420   draw_map: function() {
421     let map_lines_split = [];
422     let line = [];
423     let map_content = game.map;
424     if (this.map_mode == 'control') {
425         map_content = game.map_control;
426     }
427     for (let i = 0, j = 0; i < game.map.length; i++, j++) {
428         if (j == game.map_size[1]) {
429             map_lines_split.push(line);
430             line = [];
431             j = 0;
432         };
433         line.push(map_content[i]);
434     };
435     map_lines_split.push(line);
436     if (this.map_mode == 'terrain') {
437         for (const thing_id in game.things) {
438             let t = game.things[thing_id];
439             map_lines_split[t.position[0]][t.position[1]] = '@';
440         };
441     }
442     if (tui.mode.shows_info) {
443         map_lines_split[explorer.position[0]][explorer.position[1]] = '?';
444     }
445     let map_lines = []
446     if (game.map_geometry == 'Square') {
447         for (let line_split of map_lines_split) {
448             map_lines.push(line_split.join(' '));
449         };
450     } else if (game.map_geometry == 'Hex') {
451         let indent = 0
452         for (let line_split of map_lines_split) {
453             map_lines.push(' '.repeat(indent) + line_split.join(' '));
454             if (indent == 0) {
455                 indent = 1;
456             } else {
457                 indent = 0;
458             };
459         };
460     }
461     let window_center = [terminal.rows / 2, this.window_width / 2];
462     let player = game.things[game.player_id];
463     let center_position = [player.position[0], player.position[1]];
464     if (tui.mode.shows_info) {
465         center_position = [explorer.position[0], explorer.position[1]];
466     }
467     center_position[1] = center_position[1] * 2;
468     let offset = [center_position[0] - window_center[0],
469                   center_position[1] - window_center[1]]
470     if (game.map_geometry == 'Hex' && offset[0] % 2) {
471         offset[1] += 1;
472     };
473     let term_y = Math.max(0, -offset[0]);
474     let term_x = Math.max(0, -offset[1]);
475     let map_y = Math.max(0, offset[0]);
476     let map_x = Math.max(0, offset[1]);
477     for (; term_y < terminal.rows && map_y < game.map_size[0]; term_y++, map_y++) {
478         let to_draw = map_lines[map_y].slice(map_x, this.window_width + offset[1]);
479         terminal.write(term_y, term_x, to_draw);
480     }
481   },
482   draw_mode_line: function() {
483       let help = 'hit [' + this.keys.help + '] for help';
484       if (this.mode.has_input_prompt) {
485           help = 'enter /help for help';
486       }
487       terminal.write(0, this.window_width, 'MODE: ' + this.mode.name + ' – ' + help);
488   },
489   draw_turn_line: function(n) {
490     terminal.write(1, this.window_width, 'TURN: ' + game.turn);
491   },
492   draw_history: function() {
493       let log_display_lines = [];
494       for (let line of this.log) {
495           log_display_lines = log_display_lines.concat(this.msg_into_lines_of_width(line, this.window_width));
496       };
497       for (let y = terminal.rows - 1 - this.height_input,
498                i = log_display_lines.length - 1;
499            y >= this.height_header && i >= 0;
500            y--, i--) {
501           terminal.write(y, this.window_width, log_display_lines[i]);
502       }
503   },
504   draw_info: function() {
505     let lines = this.msg_into_lines_of_width(explorer.get_info(), this.window_width);
506     for (let y = this.height_header, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
507       terminal.write(y, this.window_width, lines[i]);
508     }
509   },
510   draw_input: function() {
511     if (this.mode.has_input_prompt) {
512         for (let y = terminal.rows - this.height_input, i = 0; i < this.input_lines.length; y++, i++) {
513             terminal.write(y, this.window_width, this.input_lines[i]);
514         }
515     }
516   },
517   draw_help: function() {
518       let movement_keys_desc = Object.keys(this.movement_keys).join(',');
519       let content = this.mode.name + " mode help (hit any key to disappear)\n\n" + this.mode.help_intro + "\n\n";
520       if (this.mode == mode_play) {
521           content += "Available actions:\n";
522           if (game.tasks.includes('MOVE')) {
523               content += "[" + movement_keys_desc + "] – move player\n";
524           }
525           if (game.tasks.includes('FLATTEN_SURROUNDINGS')) {
526               content += "[" + tui.keys.flatten + "] – flatten player's surroundings\n";
527           }
528           content += '\nOther modes available from here:\n';
529           content += '[' + this.keys.switch_to_edit + '] – terrain edit mode\n';
530           content += '[' + this.keys.switch_to_password + '] – terrain password edit mode\n';
531           content += '[' + this.keys.switch_to_chat + '] – chat mode\n';
532           content += '[' + this.keys.switch_to_study + '] – study mode\n';
533       } else if (this.mode == mode_study) {
534           content += "Available actions:\n";
535           content += '[' + movement_keys_desc + '] – move question mark\n';
536           content += '[' + this.keys.toggle_map_mode + '] – toggle view between terrain, and password protection areas\n';
537           content += '\nOther modes available from here:';
538           content += '[' + this.keys.switch_to_chat + '] – chat mode\n';
539           content += '[' + this.keys.switch_to_play + '] – play mode\n';
540           content += '[' + this.keys.switch_to_portal + '] – portal mode\n';
541           content += '[' + this.keys.switch_to_annotate + '] – annotation mode\n';
542       } else if (this.mode == mode_chat) {
543           content += '/nick NAME – re-name yourself to NAME\n';
544           content += '/msg USER TEXT – send TEXT to USER\n';
545           content += '/' + this.keys.switch_to_play + ' or /play – switch to play mode\n';
546           content += '/' + this.keys.switch_to_study + ' or /study – switch to study mode\n';
547       }
548       let start_x = 0;
549       if (!this.mode.has_input_prompt) {
550           start_x = this.window_width
551       }
552       terminal.drawBox(0, start_x, terminal.rows, this.window_width);
553       let lines = this.msg_into_lines_of_width(content, this.window_width);
554       for (let y = 0, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
555           terminal.write(y, start_x, lines[i]);
556       }
557   },
558   full_refresh: function() {
559     terminal.drawBox(0, 0, terminal.rows, terminal.cols);
560     if (this.mode.is_intro) {
561         this.draw_history();
562         this.draw_input();
563     } else {
564         if (game.turn_complete) {
565             this.draw_map();
566             this.draw_turn_line();
567         }
568         this.draw_mode_line();
569         if (this.mode.shows_info) {
570           this.draw_info();
571         } else {
572           this.draw_history();
573         }
574         this.draw_input();
575     }
576     if (this.show_help) {
577         this.draw_help();
578     }
579     terminal.refresh();
580   }
581 }
582
583 let game = {
584     init: function() {
585         this.things = {};
586         this.turn = -1;
587         this.map = "";
588         this.map_control = "";
589         this.map_size = [0,0];
590         this.player_id = -1;
591         this.portals = {};
592         this.tasks = {};
593     },
594     get_thing: function(id_, create_if_not_found=false) {
595         if (id_ in game.things) {
596             return game.things[id_];
597         } else if (create_if_not_found) {
598             let t = new Thing([0,0]);
599             game.things[id_] = t;
600             return t;
601         };
602     },
603     move: function(start_position, direction) {
604         let target = [start_position[0], start_position[1]];
605         if (direction == 'LEFT') {
606             target[1] -= 1;
607         } else if (direction == 'RIGHT') {
608             target[1] += 1;
609         } else if (game.map_geometry == 'Square') {
610             if (direction == 'UP') {
611                 target[0] -= 1;
612             } else if (direction == 'DOWN') {
613                 target[0] += 1;
614             };
615         } else if (game.map_geometry == 'Hex') {
616             let start_indented = start_position[0] % 2;
617             if (direction == 'UPLEFT') {
618                 target[0] -= 1;
619                 if (!start_indented) {
620                     target[1] -= 1;
621                 }
622             } else if (direction == 'UPRIGHT') {
623                 target[0] -= 1;
624                 if (start_indented) {
625                     target[1] += 1;
626                 }
627             } else if (direction == 'DOWNLEFT') {
628                 target[0] += 1;
629                 if (!start_indented) {
630                     target[1] -= 1;
631                 }
632             } else if (direction == 'DOWNRIGHT') {
633                 target[0] += 1;
634                 if (start_indented) {
635                     target[1] += 1;
636                 }
637             };
638         };
639         if (target[0] < 0 || target[1] < 0 ||
640             target[0] >= this.map_size[0] || target[1] >= this.map_size[1]) {
641             return null;
642         };
643         return target;
644     }
645 }
646
647 game.init();
648 tui.init();
649 tui.full_refresh();
650 server.init(websocket_location);
651
652 let explorer = {
653     position: [0,0],
654     info_db: {},
655     move: function(direction) {
656         let target = game.move(this.position, direction);
657         if (target) {
658             this.position = target
659             this.query_info();
660             tui.full_refresh();
661         } else {
662             terminal.blink_screen();
663         };
664     },
665     update_info_db: function(yx, str) {
666         this.info_db[yx] = str;
667         if (tui.mode == mode_study) {
668             tui.full_refresh();
669         }
670     },
671     empty_info_db: function() {
672         this.info_db = {};
673         if (tui.mode == mode_study) {
674             tui.full_refresh();
675         }
676     },
677     query_info: function() {
678         server.send(["GET_ANNOTATION", unparser.to_yx(explorer.position)]);
679     },
680     get_info: function() {
681         let info = "";
682         let position_i = this.position[0] * game.map_size[1] + this.position[1];
683         info += "TERRAIN: " + game.map[position_i] + "\n";
684         for (let t_id in game.things) {
685              let t = game.things[t_id];
686              if (t.position[0] == this.position[0] && t.position[1] == this.position[1]) {
687                  info += "PLAYER @";
688                  if (t.name_) {
689                      info += ": " + t.name_;
690                  }
691                  info += "\n";
692              }
693         }
694         if (this.position in game.portals) {
695             info += "PORTAL: " + game.portals[this.position] + "\n";
696         }
697         if (this.position in this.info_db) {
698             info += "ANNOTATIONS: " + this.info_db[this.position];
699         } else {
700             info += 'waiting …';
701         }
702         return info;
703     },
704     annotate: function(msg) {
705         if (msg.length == 0) {
706             msg = " ";  // triggers annotation deletion
707         }
708         server.send(["ANNOTATE", unparser.to_yx(explorer.position), msg]);
709     },
710     set_portal: function(msg) {
711         if (msg.length == 0) {
712             msg = " ";  // triggers portal deletion
713         }
714         server.send(["PORTAL", unparser.to_yx(explorer.position), msg]);
715     }
716 }
717
718 tui.inputEl.addEventListener('input', (event) => {
719     if (tui.mode.has_input_prompt) {
720         let max_length = tui.window_width * terminal.rows - tui.input_prompt.length;
721         if (tui.inputEl.value.length > max_length) {
722             tui.inputEl.value = tui.inputEl.value.slice(0, max_length);
723         };
724         tui.recalc_input_lines();
725         tui.full_refresh();
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     } else if (tui.mode == mode_teleport) {
730         if (['Y', 'y'].includes(tui.inputEl.value[0])) {
731             server.reconnect_to(tui.teleport_target);
732         } else {
733             tui.log_msg("@ teleportation aborted");
734             tui.switch_mode(mode_play);
735         }
736     }
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.full_refresh();
747     } else if (!tui.mode.has_input_prompt && event.key == tui.keys.help) {
748         tui.show_help = true;
749         tui.full_refresh();
750     } else if (tui.mode == mode_login && event.key == 'Enter') {
751         tui.login_name = tui.inputEl.value;
752         server.send(['LOGIN', tui.inputEl.value]);
753         tui.empty_input();
754     } else if (tui.mode == mode_portal && event.key == 'Enter') {
755         explorer.set_portal(tui.inputEl.value);
756         tui.switch_mode(mode_study, true);
757     } else if (tui.mode == mode_annotate && event.key == 'Enter') {
758         explorer.annotate(tui.inputEl.value);
759         tui.switch_mode(mode_study, true);
760     } else if (tui.mode == mode_password && event.key == 'Enter') {
761         if (tui.inputEl.value.length == 0) {
762             tui.inputEl.value = " ";
763         }
764         tui.password = tui.inputEl.value
765         tui.switch_mode(mode_play);
766     } else if (tui.mode == mode_teleport && event.key == 'Enter') {
767         if (tui.inputEl.value == 'YES!') {
768             server.reconnect_to(tui.teleport_target);
769         } else {
770             tui.log_msg('@ teleport aborted');
771             tui.switch_mode(mode_play);
772         };
773     } else if (tui.mode == mode_chat && event.key == 'Enter') {
774         let [tokens, token_starts] = parser.tokenize(tui.inputEl.value);
775         if (tokens.length > 0 && tokens[0].length > 0) {
776             if (tui.inputEl.value[0][0] == '/') {
777                 if (tokens[0].slice(1) == 'play' || tokens[0][1] == tui.keys.switch_to_play) {
778                     tui.switch_mode(mode_play);
779                 } else if (tokens[0].slice(1) == 'study' || tokens[0][1] == tui.keys.switch_to_study) {
780                     tui.switch_mode(mode_study);
781                 } else if (tokens[0].slice(1) == 'nick') {
782                     if (tokens.length > 1) {
783                         server.send(['NICK', tokens[1]]);
784                     } else {
785                         tui.log_msg('? need new name');
786                     }
787                 } else if (tokens[0].slice(1) == 'msg') {
788                     if (tokens.length > 2) {
789                         let msg = tui.inputEl.value.slice(token_starts[2]);
790                         server.send(['QUERY', tokens[1], msg]);
791                     } else {
792                         tui.log_msg('? need message target and message');
793                     }
794                 } else if (tokens[0].slice(1) == 'reconnect') {
795                     if (tokens.length > 1) {
796                         server.reconnect_to(tokens[1]);
797                     } else {
798                         server.reconnect();
799                     }
800                 } else {
801                     tui.log_msg('? unknown command');
802                 }
803             } else {
804                 server.send(['ALL', tui.inputEl.value]);
805             }
806         } else if (tui.inputEl.valuelength > 0) {
807             server.send(['ALL', tui.inputEl.value]);
808         }
809         tui.empty_input();
810         tui.full_refresh();
811       } else if (tui.mode == mode_play) {
812           if (event.key === tui.keys.switch_to_chat) {
813               event.preventDefault();
814               tui.switch_mode(mode_chat);
815           } else if (event.key === tui.keys.switch_to_edit
816                      && game.tasks.includes('WRITE')) {
817               event.preventDefault();
818               tui.switch_mode(mode_edit);
819           } else if (event.key === tui.keys.switch_to_study) {
820               tui.switch_mode(mode_study);
821           } else if (event.key === tui.keys.switch_to_password) {
822               event.preventDefault();
823               tui.switch_mode(mode_password);
824           } else if (event.key === tui.keys.flatten
825                      && game.tasks.includes('FLATTEN_SURROUNDINGS')) {
826               server.send(["TASK:FLATTEN_SURROUNDINGS", tui.password]);
827           } else if (event.key in tui.movement_keys
828                      && game.tasks.includes('MOVE')) {
829               server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
830           };
831     } else if (tui.mode == mode_study) {
832         if (event.key === tui.keys.switch_to_chat) {
833             event.preventDefault();
834             tui.switch_mode(mode_chat);
835         } else if (event.key == tui.keys.switch_to_play) {
836             tui.switch_mode(mode_play);
837         } else if (event.key === tui.keys.switch_to_portal) {
838             event.preventDefault();
839             tui.switch_mode(mode_portal);
840         } else if (event.key in tui.movement_keys) {
841             explorer.move(tui.movement_keys[event.key]);
842         } else if (event.key == tui.keys.toggle_map_mode) {
843             if (tui.map_mode == 'terrain') {
844                 tui.map_mode = 'control';
845             } else {
846                 tui.map_mode = 'terrain';
847             }
848             tui.full_refresh();
849         } else if (event.key === tui.keys.switch_to_annotate) {
850             event.preventDefault();
851             tui.switch_mode(mode_annotate);
852         };
853     }
854 }, false);
855
856 rows_selector.addEventListener('input', function() {
857     if (rows_selector.value % 4 != 0) {
858         return;
859     }
860     window.localStorage.setItem(rows_selector.id, rows_selector.value);
861     terminal.initialize();
862     tui.full_refresh();
863 }, false);
864 cols_selector.addEventListener('input', function() {
865     if (cols_selector.value % 4 != 0) {
866         return;
867     }
868     window.localStorage.setItem(cols_selector.id, cols_selector.value);
869     terminal.initialize();
870     tui.window_width = terminal.cols / 2,
871     tui.full_refresh();
872 }, false);
873 for (let key_selector of key_selectors) {
874     key_selector.addEventListener('input', function() {
875         window.localStorage.setItem(key_selector.id, key_selector.value);
876         tui.init_keys();
877     }, false);
878 }
879 window.setInterval(function() {
880     if (!(['input', 'n_cols', 'n_rows'].includes(document.activeElement.id)
881           || document.activeElement.id.startsWith('key_'))) {
882         tui.inputEl.focus();
883     }
884 }, 100);
885 </script>
886 </body></html>