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