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