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