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