home · contact · privacy
644f567008e48bc85e00db3c2e90cc507934a1c9
[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.map_mode = 'terrain';
416     this.mode = this['mode_' + mode_name];
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.empty_input();
422     this.restore_input_values();
423     document.getElementById("take_thing").disabled = true;
424     document.getElementById("drop_thing").disabled = true;
425     document.getElementById("flatten").disabled = true;
426     document.getElementById("teleport").disabled = true;
427     document.getElementById("toggle_map_mode").disabled = true;
428     document.getElementById("switch_to_chat").disabled = true;
429     document.getElementById("switch_to_play").disabled = true;
430     document.getElementById("switch_to_study").disabled = true;
431     document.getElementById("switch_to_edit").disabled = true;
432     document.getElementById("switch_to_portal").disabled = true;
433     document.getElementById("switch_to_annotate").disabled = true;
434     document.getElementById("switch_to_password").disabled = true;
435     document.getElementById("switch_to_admin").disabled = true;
436     document.getElementById("switch_to_control_pw").disabled = true;
437     document.getElementById("move_left").disabled = true;
438     document.getElementById("move_upleft").disabled = true;
439     document.getElementById("move_up").disabled = true;
440     document.getElementById("move_upright").disabled = true;
441     document.getElementById("move_downleft").disabled = true;
442     document.getElementById("move_down").disabled = true;
443     document.getElementById("move_downright").disabled = true;
444     document.getElementById("move_right").disabled = true;
445     if (this.mode.name == 'play' || this.mode.name == 'study') {
446         document.getElementById("move_left").disabled = false;
447         document.getElementById("move_right").disabled = false;
448         if (game.map_geometry == 'Hex') {
449             document.getElementById("move_upleft").disabled = false;
450             document.getElementById("move_upright").disabled = false;
451             document.getElementById("move_downleft").disabled = false;
452             document.getElementById("move_downright").disabled = false;
453         } else {
454             document.getElementById("move_up").disabled = false;
455             document.getElementById("move_down").disabled = false;
456         }
457     }
458     if (!this.mode.is_intro && this.mode.name != 'play') {
459         document.getElementById("switch_to_play").disabled = false;
460     }
461     if (!this.mode.is_intro && this.mode.name != 'study') {
462         document.getElementById("switch_to_study").disabled = false;
463     }
464     if (!this.mode.is_intro && this.mode.name != 'chat') {
465         document.getElementById("switch_to_chat").disabled = false;
466     }
467     if (this.mode.name == 'login') {
468         if (this.login_name) {
469             server.send(['LOGIN', this.login_name]);
470         } else {
471             this.log_msg("? need login name");
472         }
473     } else if (this.mode.name == 'play') {
474         if (game.tasks.includes('PICK_UP')) {
475             document.getElementById("take_thing").disabled = false;
476         }
477         if (game.tasks.includes('DROP')) {
478             document.getElementById("drop_thing").disabled = false;
479         }
480         if (game.tasks.includes('FLATTEN_SURROUNDINGS')) {
481             document.getElementById("flatten").disabled = false;
482         }
483         if (game.tasks.includes('MOVE')) {
484         }
485         document.getElementById("teleport").disabled = false;
486         document.getElementById("switch_to_annotate").disabled = false;
487         document.getElementById("switch_to_edit").disabled = false;
488         document.getElementById("switch_to_portal").disabled = false;
489         document.getElementById("switch_to_password").disabled = false;
490         document.getElementById("switch_to_admin").disabled = false;
491         document.getElementById("switch_to_control_pw").disabled = false;
492     } else if (this.mode.name == 'study') {
493         document.getElementById("toggle_map_mode").disabled = false;
494     } else if (this.mode.is_single_char_entry) {
495         this.show_help = true;
496     } else if (this.mode.name == 'admin') {
497         this.log_msg('@ enter admin password:')
498     } else if (this.mode.name == 'control_pw_pw') {
499         this.log_msg('@ enter tile control password for "' + this.tile_control_char + '":');
500     }
501     this.full_refresh();
502   },
503   restore_input_values: function() {
504       if (this.mode.name == 'annotate' && explorer.position in explorer.info_db) {
505           let info = explorer.info_db[explorer.position];
506           if (info != "(none)") {
507               this.inputEl.value = info;
508               this.recalc_input_lines();
509           }
510       } else if (this.mode.name == 'portal' && explorer.position in game.portals) {
511           let portal = game.portals[explorer.position]
512           this.inputEl.value = portal;
513           this.recalc_input_lines();
514       } else if (this.mode.name == 'password') {
515           this.inputEl.value = this.password;
516           this.recalc_input_lines();
517       }
518   },
519   empty_input: function(str) {
520       this.inputEl.value = "";
521       if (this.mode.has_input_prompt) {
522           this.recalc_input_lines();
523       } else {
524           this.height_input = 0;
525       }
526   },
527   recalc_input_lines: function() {
528       this.input_lines = this.msg_into_lines_of_width(this.input_prompt + this.inputEl.value, this.window_width);
529       this.height_input = this.input_lines.length;
530   },
531   msg_into_lines_of_width: function(msg, width) {
532     let chunk = "";
533     let lines = [];
534     for (let i = 0, x = 0; i < msg.length; i++, x++) {
535       if (x >= width || msg[i] == "\n") {
536         lines.push(chunk);
537         chunk = "";
538         x = 0;
539       };
540       if (msg[i] != "\n") {
541         chunk += msg[i];
542       }
543     }
544     lines.push(chunk);
545     return lines;
546   },
547   log_msg: function(msg) {
548       this.log.push(msg);
549       while (this.log.length > 100) {
550         this.log.shift();
551       };
552       this.full_refresh();
553   },
554   draw_map: function() {
555     let map_lines_split = [];
556     let line = [];
557     let map_content = game.map;
558     if (this.map_mode == 'control') {
559         map_content = game.map_control;
560     }
561     for (let i = 0, j = 0; i < game.map.length; i++, j++) {
562         if (j == game.map_size[1]) {
563             map_lines_split.push(line);
564             line = [];
565             j = 0;
566         };
567         line.push(map_content[i] + ' ');
568     };
569     map_lines_split.push(line);
570     if (this.map_mode == 'annotations') {
571         for (const coordinate of explorer.info_hints) {
572             map_lines_split[coordinate[0]][coordinate[1]] = 'A ';
573         }
574     } else if (this.map_mode == 'terrain') {
575         for (const p in game.portals) {
576             let coordinate = p.split(',')
577             map_lines_split[coordinate[0]][coordinate[1]] = 'P ';
578         }
579         let used_positions = [];
580         for (const thing_id in game.things) {
581             let t = game.things[thing_id];
582             let symbol = game.thing_types[t.type_];
583             let meta_char = ' ';
584             if (t.player_char) {
585                 meta_char = t.player_char;
586             }
587             if (used_positions.includes(t.position.toString())) {
588                 meta_char = '+';
589             };
590             map_lines_split[t.position[0]][t.position[1]] = symbol + meta_char;
591             used_positions.push(t.position.toString());
592         };
593     }
594     if (tui.mode.shows_info) {
595         map_lines_split[explorer.position[0]][explorer.position[1]] = '??';
596     }
597     let map_lines = []
598     if (game.map_geometry == 'Square') {
599         for (let line_split of map_lines_split) {
600             map_lines.push(line_split.join(''));
601         };
602     } else if (game.map_geometry == 'Hex') {
603         let indent = 0
604         for (let line_split of map_lines_split) {
605             map_lines.push(' '.repeat(indent) + line_split.join(''));
606             if (indent == 0) {
607                 indent = 1;
608             } else {
609                 indent = 0;
610             };
611         };
612     }
613     let window_center = [terminal.rows / 2, this.window_width / 2];
614     let player = game.things[game.player_id];
615     let center_position = [player.position[0], player.position[1]];
616     if (tui.mode.shows_info) {
617         center_position = [explorer.position[0], explorer.position[1]];
618     }
619     center_position[1] = center_position[1] * 2;
620     let offset = [center_position[0] - window_center[0],
621                   center_position[1] - window_center[1]]
622     if (game.map_geometry == 'Hex' && offset[0] % 2) {
623         offset[1] += 1;
624     };
625     let term_y = Math.max(0, -offset[0]);
626     let term_x = Math.max(0, -offset[1]);
627     let map_y = Math.max(0, offset[0]);
628     let map_x = Math.max(0, offset[1]);
629     for (; term_y < terminal.rows && map_y < game.map_size[0]; term_y++, map_y++) {
630         let to_draw = map_lines[map_y].slice(map_x, this.window_width + offset[1]);
631         terminal.write(term_y, term_x, to_draw);
632     }
633   },
634   draw_mode_line: function() {
635       let help = 'hit [' + this.keys.help + '] for help';
636       if (this.mode.has_input_prompt) {
637           help = 'enter /help for help';
638       }
639       terminal.write(0, this.window_width, 'MODE: ' + this.mode.name + ' – ' + help);
640   },
641   draw_turn_line: function(n) {
642     terminal.write(1, this.window_width, 'TURN: ' + game.turn);
643   },
644   draw_history: function() {
645       let log_display_lines = [];
646       for (let line of this.log) {
647           log_display_lines = log_display_lines.concat(this.msg_into_lines_of_width(line, this.window_width));
648       };
649       for (let y = terminal.rows - 1 - this.height_input,
650                i = log_display_lines.length - 1;
651            y >= this.height_header && i >= 0;
652            y--, i--) {
653           terminal.write(y, this.window_width, log_display_lines[i]);
654       }
655   },
656   draw_info: function() {
657     let lines = this.msg_into_lines_of_width(explorer.get_info(), this.window_width);
658     for (let y = this.height_header, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
659       terminal.write(y, this.window_width, lines[i]);
660     }
661   },
662   draw_input: function() {
663     if (this.mode.has_input_prompt) {
664         for (let y = terminal.rows - this.height_input, i = 0; i < this.input_lines.length; y++, i++) {
665             terminal.write(y, this.window_width, this.input_lines[i]);
666         }
667     }
668   },
669   draw_help: function() {
670       let movement_keys_desc = Object.keys(this.movement_keys).join(',');
671       let content = this.mode.name + " mode help\n\n" + this.mode.help_intro + "\n\n";
672       if (this.mode.name == 'play') {
673           content += "Available actions:\n";
674           if (game.tasks.includes('MOVE')) {
675               content += "[" + movement_keys_desc + "] – move player\n";
676           }
677           if (game.tasks.includes('PICK_UP')) {
678               content += "[" + this.keys.take_thing + "] – take thing under player\n";
679           }
680           if (game.tasks.includes('DROP')) {
681               content += "[" + this.keys.drop_thing + "] – drop carried thing\n";
682           }
683           if (game.tasks.includes('FLATTEN_SURROUNDINGS')) {
684               content += "[" + tui.keys.flatten + "] – flatten player's surroundings\n";
685           }
686           content += "[" + tui.keys.teleport + "] – teleport to other space\n";
687           content += '\nOther modes available from here:\n';
688           content += '[' + this.keys.switch_to_chat + '] – chat mode\n';
689           content += '[' + this.keys.switch_to_study + '] – study mode\n';
690           content += '[' + this.keys.switch_to_edit + '] – terrain edit mode\n';
691           content += '[' + this.keys.switch_to_portal + '] – portal edit mode\n';
692           content += '[' + this.keys.switch_to_annotate + '] – annotation mode\n';
693           content += '[' + this.keys.switch_to_password + '] – password input mode\n';
694           content += '[' + this.keys.switch_to_admin + '] – become admin\n';
695           content += '[' + this.keys.switch_to_control_pw + '] – change tile control password\n';
696       } else if (this.mode.name == 'study') {
697           content += "Available actions:\n";
698           content += '[' + movement_keys_desc + '] – move question mark\n';
699           content += '[' + this.keys.toggle_map_mode + '] – toggle view between terrain, annotations, and password protection areas\n';
700           content += '\nOther modes available from here:\n';
701           content += '[' + this.keys.switch_to_chat + '] – chat mode\n';
702           content += '[' + this.keys.switch_to_play + '] – play mode\n';
703       } else if (this.mode.name == 'chat') {
704           content += '/nick NAME – re-name yourself to NAME\n';
705           content += '/' + this.keys.switch_to_play + ' or /play – switch to play mode\n';
706           content += '/' + this.keys.switch_to_study + ' or /study – switch to study mode\n';
707       }
708       let start_x = 0;
709       if (!this.mode.has_input_prompt) {
710           start_x = this.window_width
711       }
712       terminal.drawBox(0, start_x, terminal.rows, this.window_width);
713       let lines = this.msg_into_lines_of_width(content, this.window_width);
714       for (let y = 0, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
715           terminal.write(y, start_x, lines[i]);
716       }
717   },
718   full_refresh: function() {
719     terminal.drawBox(0, 0, terminal.rows, terminal.cols);
720     if (this.mode.is_intro) {
721         this.draw_history();
722         this.draw_input();
723     } else {
724         if (game.turn_complete) {
725             this.draw_map();
726             this.draw_turn_line();
727         }
728         this.draw_mode_line();
729         if (this.mode.shows_info) {
730           this.draw_info();
731         } else {
732           this.draw_history();
733         }
734         this.draw_input();
735     }
736     if (this.show_help) {
737         this.draw_help();
738     }
739     terminal.refresh();
740   }
741 }
742
743 let game = {
744     init: function() {
745         this.things = {};
746         this.turn = -1;
747         this.map = "";
748         this.map_control = "";
749         this.map_size = [0,0];
750         this.player_id = -1;
751         this.portals = {};
752         this.tasks = {};
753     },
754     get_thing: function(id_, create_if_not_found=false) {
755         if (id_ in game.things) {
756             return game.things[id_];
757         } else if (create_if_not_found) {
758             let t = new Thing([0,0]);
759             game.things[id_] = t;
760             return t;
761         };
762     },
763     move: function(start_position, direction) {
764         let target = [start_position[0], start_position[1]];
765         if (direction == 'LEFT') {
766             target[1] -= 1;
767         } else if (direction == 'RIGHT') {
768             target[1] += 1;
769         } else if (game.map_geometry == 'Square') {
770             if (direction == 'UP') {
771                 target[0] -= 1;
772             } else if (direction == 'DOWN') {
773                 target[0] += 1;
774             };
775         } else if (game.map_geometry == 'Hex') {
776             let start_indented = start_position[0] % 2;
777             if (direction == 'UPLEFT') {
778                 target[0] -= 1;
779                 if (!start_indented) {
780                     target[1] -= 1;
781                 }
782             } else if (direction == 'UPRIGHT') {
783                 target[0] -= 1;
784                 if (start_indented) {
785                     target[1] += 1;
786                 }
787             } else if (direction == 'DOWNLEFT') {
788                 target[0] += 1;
789                 if (!start_indented) {
790                     target[1] -= 1;
791                 }
792             } else if (direction == 'DOWNRIGHT') {
793                 target[0] += 1;
794                 if (start_indented) {
795                     target[1] += 1;
796                 }
797             };
798         };
799         if (target[0] < 0 || target[1] < 0 ||
800             target[0] >= this.map_size[0] || target[1] >= this.map_size[1]) {
801             return null;
802         };
803         return target;
804     },
805     teleport: function() {
806         let player = this.get_thing(game.player_id);
807         if (player.position in this.portals) {
808             server.reconnect_to(this.portals[player.position]);
809         } else {
810             terminal.blink_screen();
811             tui.log_msg('? not standing on portal')
812         }
813     }
814 }
815
816 game.init();
817 tui.init();
818 tui.full_refresh();
819 server.init(websocket_location);
820
821 let explorer = {
822     position: [0,0],
823     info_db: {},
824     info_hints: [],
825     move: function(direction) {
826         let target = game.move(this.position, direction);
827         if (target) {
828             this.position = target
829             this.query_info();
830         } else {
831             terminal.blink_screen();
832         };
833     },
834     update_info_db: function(yx, str) {
835         this.info_db[yx] = str;
836         if (tui.mode.name == 'study') {
837             tui.full_refresh();
838         }
839     },
840     empty_info_db: function() {
841         this.info_db = {};
842         this.info_hints = [];
843         if (tui.mode.name == 'study') {
844             tui.full_refresh();
845         }
846     },
847     query_info: function() {
848         server.send(["GET_ANNOTATION", unparser.to_yx(explorer.position)]);
849     },
850     get_info: function() {
851         let position_i = this.position[0] * game.map_size[1] + this.position[1];
852         if (game.fov[position_i] != '.') {
853             return 'outside field of view';
854         };
855         let info = "";
856         let terrain_char = game.map[position_i]
857         let terrain_desc = '?'
858         if (game.terrains[terrain_char]) {
859             terrain_desc = game.terrains[terrain_char];
860         };
861         info += 'TERRAIN: "' + terrain_char + '" / ' + terrain_desc + "\n";
862         let protection = game.map_control[position_i];
863         if (protection == '.') {
864             protection = 'unprotected';
865         };
866         info += 'PROTECTION: ' + protection + '\n';
867         for (let t_id in game.things) {
868              let t = game.things[t_id];
869              if (t.position[0] == this.position[0] && t.position[1] == this.position[1]) {
870                  let symbol = game.thing_types[t.type_];
871                  info += "THING: " + t.type_ + " / " + symbol;
872                  if (t.player_char) {
873                      info += t.player_char;
874                  };
875                  if (t.name_) {
876                      info += " (" + t.name_ + ")";
877                  }
878                  info += "\n";
879              }
880         }
881         if (this.position in game.portals) {
882             info += "PORTAL: " + game.portals[this.position] + "\n";
883         }
884         if (this.position in this.info_db) {
885             info += "ANNOTATIONS: " + this.info_db[this.position];
886         } else {
887             info += 'waiting …';
888         }
889         return info;
890     },
891     annotate: function(msg) {
892         if (msg.length == 0) {
893             msg = " ";  // triggers annotation deletion
894         }
895         server.send(["ANNOTATE", unparser.to_yx(explorer.position), msg, tui.password]);
896     },
897     set_portal: function(msg) {
898         if (msg.length == 0) {
899             msg = " ";  // triggers portal deletion
900         }
901         server.send(["PORTAL", unparser.to_yx(explorer.position), msg, tui.password]);
902     }
903 }
904
905 tui.inputEl.addEventListener('input', (event) => {
906     if (tui.mode.has_input_prompt) {
907         let max_length = tui.window_width * terminal.rows - tui.input_prompt.length;
908         if (tui.inputEl.value.length > max_length) {
909             tui.inputEl.value = tui.inputEl.value.slice(0, max_length);
910         };
911         tui.recalc_input_lines();
912     } else if (tui.mode.name == 'edit' && tui.inputEl.value.length > 0) {
913         server.send(["TASK:WRITE", tui.inputEl.value[0], tui.password]);
914         tui.switch_mode('play');
915     } else if (tui.mode.name == 'control_pw_type' && tui.inputEl.value.length > 0) {
916         tui.tile_control_char = tui.inputEl.value[0];
917         tui.switch_mode('control_pw_pw');
918     }
919     tui.full_refresh();
920 }, false);
921 tui.inputEl.addEventListener('keydown', (event) => {
922     tui.show_help = false;
923     if (event.key == 'Enter') {
924         event.preventDefault();
925     }
926     if (tui.mode.has_input_prompt && event.key == 'Enter' && tui.inputEl.value == '/help') {
927         tui.show_help = true;
928         tui.empty_input();
929         tui.restore_input_values();
930     } else if (!tui.mode.has_input_prompt && event.key == tui.keys.help
931                && !tui.mode.is_single_char_entry) {
932         tui.show_help = true;
933     } else if (tui.mode.name == 'login' && event.key == 'Enter') {
934         tui.login_name = tui.inputEl.value;
935         server.send(['LOGIN', tui.inputEl.value]);
936         tui.empty_input();
937     } else if (tui.mode.name == 'control_pw_pw' && event.key == 'Enter') {
938         if (tui.inputEl.value.length == 0) {
939             tui.log_msg('@ aborted');
940         } else {
941             server.send(['SET_MAP_CONTROL_PASSWORD',
942                         tui.tile_control_char, tui.inputEl.value]);
943         }
944         tui.switch_mode('play');
945     } else if (tui.mode.name == 'portal' && event.key == 'Enter') {
946         explorer.set_portal(tui.inputEl.value);
947         tui.switch_mode('play');
948     } else if (tui.mode.name == 'annotate' && event.key == 'Enter') {
949         explorer.annotate(tui.inputEl.value);
950         tui.switch_mode('play');
951     } else if (tui.mode.name == 'password' && event.key == 'Enter') {
952         if (tui.inputEl.value.length == 0) {
953             tui.inputEl.value = " ";
954         }
955         tui.password = tui.inputEl.value
956         tui.switch_mode('play');
957     } else if (tui.mode.name == 'admin' && event.key == 'Enter') {
958         server.send(['BECOME_ADMIN', tui.inputEl.value]);
959         tui.switch_mode('play');
960     } else if (tui.mode.name == 'chat' && event.key == 'Enter') {
961         let tokens = parser.tokenize(tui.inputEl.value);
962         if (tokens.length > 0 && tokens[0].length > 0) {
963             if (tui.inputEl.value[0][0] == '/') {
964                 if (tokens[0].slice(1) == 'play' || tokens[0][1] == tui.keys.switch_to_play) {
965                     tui.switch_mode('play');
966                 } else if (tokens[0].slice(1) == 'study' || tokens[0][1] == tui.keys.switch_to_study) {
967                     tui.switch_mode('study');
968                 } else if (tokens[0].slice(1) == 'nick') {
969                     if (tokens.length > 1) {
970                         server.send(['NICK', tokens[1]]);
971                     } else {
972                         tui.log_msg('? need new name');
973                     }
974                 } else {
975                     tui.log_msg('? unknown command');
976                 }
977             } else {
978                     server.send(['ALL', tui.inputEl.value]);
979             }
980         } else if (tui.inputEl.valuelength > 0) {
981                 server.send(['ALL', tui.inputEl.value]);
982         }
983         tui.empty_input();
984     } else if (tui.mode.name == 'play') {
985           if (event.key === tui.keys.switch_to_chat) {
986               event.preventDefault();
987               tui.switch_mode('chat');
988           } else if (event.key === tui.keys.switch_to_edit
989                      && game.tasks.includes('WRITE')) {
990               event.preventDefault();
991               tui.switch_mode('edit');
992           } else if (event.key === tui.keys.switch_to_study) {
993               tui.switch_mode('study');
994           } else if (event.key === tui.keys.switch_to_admin) {
995               event.preventDefault();
996               tui.switch_mode('admin');
997           } else if (event.key === tui.keys.switch_to_control_pw) {
998               event.preventDefault();
999               tui.switch_mode('control_pw_type');
1000           } else if (event.key === tui.keys.switch_to_password) {
1001               event.preventDefault();
1002               tui.switch_mode('password');
1003           } else if (event.key === tui.keys.flatten
1004                      && game.tasks.includes('FLATTEN_SURROUNDINGS')) {
1005               server.send(["TASK:FLATTEN_SURROUNDINGS", tui.password]);
1006           } else if (event.key === tui.keys.take_thing
1007                      && game.tasks.includes('PICK_UP')) {
1008               server.send(["TASK:PICK_UP"]);
1009           } else if (event.key === tui.keys.drop_thing
1010                      && game.tasks.includes('DROP')) {
1011               server.send(["TASK:DROP"]);
1012           } else if (event.key in tui.movement_keys
1013                      && game.tasks.includes('MOVE')) {
1014               server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1015           } else if (event.key === tui.keys.teleport) {
1016               game.teleport();
1017           } else if (event.key === tui.keys.switch_to_portal) {
1018               event.preventDefault();
1019               tui.switch_mode('portal');
1020           } else if (event.key === tui.keys.switch_to_annotate) {
1021               event.preventDefault();
1022               tui.switch_mode('annotate');
1023           };
1024     } else if (tui.mode.name == 'study') {
1025         if (event.key === tui.keys.switch_to_chat) {
1026             event.preventDefault();
1027             tui.switch_mode('chat');
1028         } else if (event.key == tui.keys.switch_to_play) {
1029             tui.switch_mode('play');
1030         } else if (event.key in tui.movement_keys) {
1031             explorer.move(tui.movement_keys[event.key]);
1032         } else if (event.key == tui.keys.toggle_map_mode) {
1033             if (tui.map_mode == 'terrain') {
1034                 tui.map_mode = 'annotations';
1035             } else if (tui.map_mode == 'annotations') {
1036                 tui.map_mode = 'control';
1037             } else {
1038                 tui.map_mode = 'terrain';
1039             }
1040         };
1041     }
1042     tui.full_refresh();
1043 }, false);
1044
1045 rows_selector.addEventListener('input', function() {
1046     if (rows_selector.value % 4 != 0 || rows_selector.value < 24) {
1047         return;
1048     }
1049     window.localStorage.setItem(rows_selector.id, rows_selector.value);
1050     terminal.initialize();
1051     tui.full_refresh();
1052 }, false);
1053 cols_selector.addEventListener('input', function() {
1054     if (cols_selector.value % 4 != 0 || cols_selector.value < 80) {
1055         return;
1056     }
1057     window.localStorage.setItem(cols_selector.id, cols_selector.value);
1058     terminal.initialize();
1059     tui.window_width = terminal.cols / 2,
1060     tui.full_refresh();
1061 }, false);
1062 for (let key_selector of key_selectors) {
1063     key_selector.addEventListener('input', function() {
1064         window.localStorage.setItem(key_selector.id, key_selector.value);
1065         tui.init_keys();
1066     }, false);
1067 }
1068 window.setInterval(function() {
1069     if (server.connected) {
1070         server.send(['PING']);
1071     } else {
1072         server.reconnect_to(server.url);
1073         tui.log_msg('@ attempting reconnect …')
1074     }
1075 }, 5000);
1076 document.getElementById("terminal").onclick = function() {
1077     tui.inputEl.focus();
1078 };
1079 document.getElementById("help").onclick = function() {
1080     tui.show_help = true;
1081     tui.full_refresh();
1082 };
1083 document.getElementById("switch_to_play").onclick = function() {
1084     tui.switch_mode('play');
1085     tui.full_refresh();
1086 };
1087 document.getElementById("switch_to_study").onclick = function() {
1088     tui.switch_mode('study');
1089     tui.full_refresh();
1090 };
1091 document.getElementById("switch_to_chat").onclick = function() {
1092     tui.switch_mode('chat');
1093     tui.full_refresh();
1094 };
1095 document.getElementById("switch_to_password").onclick = function() {
1096     tui.switch_mode('password');
1097     tui.full_refresh();
1098 };
1099 document.getElementById("switch_to_edit").onclick = function() {
1100     tui.switch_mode('edit');
1101     tui.full_refresh();
1102 };
1103 document.getElementById("switch_to_annotate").onclick = function() {
1104     tui.switch_mode('annotate');
1105     tui.full_refresh();
1106 };
1107 document.getElementById("switch_to_portal").onclick = function() {
1108     tui.switch_mode('portal');
1109     tui.full_refresh();
1110 };
1111 document.getElementById("switch_to_admin").onclick = function() {
1112     tui.switch_mode('admin');
1113     tui.full_refresh();
1114 };
1115 document.getElementById("switch_to_control_pw").onclick = function() {
1116     tui.switch_mode('control_pw_type');
1117     tui.full_refresh();
1118 };
1119 document.getElementById("toggle_map_mode").onclick = function() {
1120     if (tui.map_mode == 'terrain') {
1121         tui.map_mode = 'annotations';
1122     } else if (tui.map_mode == 'annotations') {
1123         tui.map_mode = 'control';
1124     } else {
1125         tui.map_mode = 'terrain';
1126     }
1127     tui.full_refresh();
1128 };
1129 document.getElementById("take_thing").onclick = function() {
1130         server.send(['TASK:PICK_UP']);
1131 };
1132 document.getElementById("drop_thing").onclick = function() {
1133         server.send(['TASK:DROP']);
1134 };
1135 document.getElementById("flatten").onclick = function() {
1136     server.send(['TASK:FLATTEN_SURROUNDINGS', tui.password]);
1137 };
1138 document.getElementById("teleport").onclick = function() {
1139     game.teleport();
1140 };
1141 document.getElementById("move_upleft").onclick = function() {
1142     if (tui.mode.name == 'play') {
1143         server.send(['TASK:MOVE', 'UPLEFT']);
1144     } else {
1145         explorer.move('UPLEFT');
1146     };
1147 };
1148 document.getElementById("move_left").onclick = function() {
1149     if (tui.mode.name == 'play') {
1150         server.send(['TASK:MOVE', 'LEFT']);
1151     } else {
1152         explorer.move('LEFT');
1153     };
1154 };
1155 document.getElementById("move_downleft").onclick = function() {
1156     if (tui.mode.name == 'play') {
1157         server.send(['TASK:MOVE', 'DOWNLEFT']);
1158     } else {
1159         explorer.move('DOWNLEFT');
1160     };
1161 };
1162 document.getElementById("move_down").onclick = function() {
1163     if (tui.mode.name == 'play') {
1164         server.send(['TASK:MOVE', 'DOWN']);
1165     } else {
1166         explorer.move('DOWN');
1167     };
1168 };
1169 document.getElementById("move_up").onclick = function() {
1170     if (tui.mode.name == 'play') {
1171         server.send(['TASK:MOVE', 'UP']);
1172     } else {
1173         explorer.move('UP');
1174     };
1175 };
1176 document.getElementById("move_upright").onclick = function() {
1177     if (tui.mode.name == 'play') {
1178         server.send(['TASK:MOVE', 'UPRIGHT']);
1179     } else {
1180         explorer.move('UPRIGHT');
1181     };
1182 };
1183 document.getElementById("move_right").onclick = function() {
1184     if (tui.mode.name == 'play') {
1185         server.send(['TASK:MOVE', 'RIGHT']);
1186     } else {
1187         explorer.move('RIGHT');
1188     };
1189 };
1190 document.getElementById("move_downright").onclick = function() {
1191     if (tui.mode.name == 'play') {
1192         server.send(['TASK:MOVE', 'DOWNRIGHT']);
1193     } else {
1194         explorer.move('DOWNRIGHT');
1195     };
1196 };
1197 </script>
1198 </body></html>