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