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