home · contact · privacy
a5b2d69d81913938ba42c2ec4b9bee549e7779df
[plomrogue2] / rogue_chat_nocanvas_monochrome.html
1 <!DOCTYPE html>
2 <html><head>
3 <style>
4 </style>
5 </head><body>
6 <div>
7 terminal rows: <input id="n_rows" type="number" step=4 min=24 value=24 />
8 terminal columns: <input id="n_cols" type="number" step=4 min=80 value=80 />
9 </div>
10 <pre id="terminal" style="display: inline-block;"></pre>
11 <textarea id="input" style="opacity: 0; width: 0px;"></textarea>
12 <div>
13 <h3>for mouse players</h3>
14 <table style="float: left">
15 <tr><td><button id="move_upleft">up-left</button></td><td><button id="move_up">up</button></td><td><button id="move_upright">up-right</button></td></tr>
16 <tr><td><button id="move_left">left</button></td><td>MOVE</td><td><button id="move_right">right</button></td></tr>
17 <tr><td><button id="move_downleft">down-left</button></td><td><button id="move_down">down</button></td><td><button id="move_downright">down-right</button></td></tr>
18 </table>
19 <div>
20 <button id="help">help</button>
21 <button id="switch_to_play">play mode</button>
22 <button id="switch_to_study">study mode</button>
23 <button id="switch_to_chat">chat mode</button><br />
24 <button id="take_thing">take thing</button>
25 <button id="drop_thing">drop thing</button>
26 <button id="flatten">flatten surroundings</button>
27 <button id="teleport">teleport</button>
28 <button id="switch_to_edit">change tile</button><br />
29 <button id="switch_to_password">change tile editing password</button>
30 <button id="switch_to_annotate">annotate tile</button>
31 <button id="switch_to_portal">edit portal link</button>
32 <button id="toggle_map_mode">toggle terrain/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 //let websocket_location = "ws://localhost:8000/";
65
66 let rows_selector = document.getElementById("n_rows");
67 let cols_selector = document.getElementById("n_cols");
68 let key_selectors = document.querySelectorAll('[id^="key_"]');
69
70 function restore_selector_value(selector) {
71     let stored_selection = window.localStorage.getItem(selector.id);
72     if (stored_selection) {
73         selector.value = stored_selection;
74     }
75 }
76 restore_selector_value(rows_selector);
77 restore_selector_value(cols_selector);
78 for (let key_selector of key_selectors) {
79     restore_selector_value(key_selector);
80 }
81
82 let terminal = {
83   foreground: 'white',
84   background: 'black',
85   initialize: function() {
86     this.rows = rows_selector.value;
87     this.cols = cols_selector.value;
88     this.pre_el = document.getElementById("terminal");
89     this.pre_el.style.color = this.foreground;
90     this.pre_el.style.backgroundColor = this.background;
91     this.content = [];
92       let line = []
93     for (let y = 0, x = 0; y <= this.rows; x++) {
94         if (x == this.cols) {
95             x = 0;
96             y += 1;
97             this.content.push(line);
98             line = [];
99             if (y == this.rows) {
100                 break;
101             }
102         }
103         line.push(' ');
104     }
105   },
106   blink_screen: function() {
107       this.pre_el.style.color = this.background;
108       this.pre_el.style.backgroundColor = this.foreground;
109       setTimeout(() => {
110           this.pre_el.style.color = this.foreground;
111           this.pre_el.style.backgroundColor = this.background;
112       }, 100);
113   },
114   refresh: function() {
115       let pre_string = '';
116       for (let y = 0; y < this.rows; y++) {
117           let line = this.content[y].join('');
118           pre_string += line + '\n';
119       }
120       this.pre_el.textContent = pre_string;
121   },
122   write: function(start_y, start_x, msg) {
123       for (let x = start_x, i = 0; x < this.cols && i < msg.length; x++, i++) {
124           this.content[start_y][x] = msg[i];
125       }
126   },
127   drawBox: function(start_y, start_x, height, width) {
128     let end_y = start_y + height;
129     let end_x = start_x + width;
130     for (let y = start_y, x = start_x; y < this.rows; x++) {
131       if (x == end_x) {
132         x = start_x;
133         y += 1;
134         if (y == end_y) {
135             break;
136         }
137       };
138       this.content[y][x] = ' ';
139     }
140   },
141 }
142 terminal.initialize();
143
144 let parser = {
145   tokenize: function(str) {
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           tokens.push(token);
168           token = '';
169         }
170       } else {
171         token += c;
172       }
173     }
174     if (token.length > 0) {
175       tokens.push(token);
176     }
177     return tokens;
178   },
179   parse_yx: function(position_string) {
180     let coordinate_strings = position_string.split(',')
181     let position = [0, 0];
182     position[0] = parseInt(coordinate_strings[0].slice(2));
183     position[1] = parseInt(coordinate_strings[1].slice(2));
184     return position;
185   },
186 }
187
188 class Thing {
189     constructor(yx) {
190         this.position = yx;
191     }
192 }
193
194 let server = {
195     init: function(url) {
196         this.url = url;
197         this.websocket = new WebSocket(this.url);
198         this.websocket.onopen = function(event) {
199             server.connected = true;
200             game.thing_types = {};
201             game.terrains = {};
202             server.send(['TASKS']);
203             server.send(['TERRAINS']);
204             server.send(['THING_TYPES']);
205             tui.log_msg("@ server connected! :)");
206             tui.switch_mode(mode_login);
207         };
208         this.websocket.onclose = function(event) {
209             server.connected = false;
210             tui.switch_mode(mode_waiting_for_server);
211             tui.log_msg("@ server disconnected :(");
212         };
213             this.websocket.onmessage = this.handle_event;
214         },
215     reconnect_to: function(url) {
216         this.websocket.close();
217         this.init(url);
218     },
219     send: function(tokens) {
220         this.websocket.send(unparser.untokenize(tokens));
221     },
222     handle_event: function(event) {
223         let tokens = parser.tokenize(event.data);
224         if (tokens[0] === 'TURN') {
225             game.turn_complete = false;
226             game.things = {};
227             game.portals = {};
228             game.turn = parseInt(tokens[1]);
229         } else if (tokens[0] === 'THING') {
230             let t = game.get_thing(tokens[3], true);
231             t.position = parser.parse_yx(tokens[1]);
232             t.type_ = tokens[2];
233         } else if (tokens[0] === 'THING_NAME') {
234             let t = game.get_thing(tokens[1], false);
235             if (t) {
236                 t.name_ = tokens[2];
237             };
238         } else if (tokens[0] === 'THING_CHAR') {
239             let t = game.get_thing(tokens[1], false);
240             if (t) {
241                 t.player_char = tokens[2];
242             };
243         } else if (tokens[0] === 'TASKS') {
244             game.tasks = tokens[1].split(',')
245         } else if (tokens[0] === 'THING_TYPE') {
246             game.thing_types[tokens[1]] = tokens[2]
247         } else if (tokens[0] === 'TERRAIN') {
248             game.terrains[tokens[1]] = tokens[2]
249         } else if (tokens[0] === 'MAP') {
250             game.map_geometry = tokens[1];
251             tui.init_keys();
252             game.map_size = parser.parse_yx(tokens[2]);
253             game.map = tokens[3]
254         } else if (tokens[0] === 'FOV') {
255             game.fov = tokens[1]
256         } else if (tokens[0] === 'MAP_CONTROL') {
257             game.map_control = tokens[1]
258         } else if (tokens[0] === 'GAME_STATE_COMPLETE') {
259             game.turn_complete = true;
260             explorer.empty_info_db();
261             if (tui.mode == mode_post_login_wait) {
262                 tui.switch_mode(mode_play);
263             } else if (tui.mode == mode_study) {
264                 explorer.query_info();
265             }
266             tui.full_refresh();
267         } else if (tokens[0] === 'CHAT') {
268              tui.log_msg('# ' + tokens[1], 1);
269         } else if (tokens[0] === 'PLAYER_ID') {
270             game.player_id = parseInt(tokens[1]);
271         } else if (tokens[0] === 'LOGIN_OK') {
272             this.send(['GET_GAMESTATE']);
273             tui.switch_mode(mode_post_login_wait);
274         } else if (tokens[0] === 'PORTAL') {
275             let position = parser.parse_yx(tokens[1]);
276             game.portals[position] = tokens[2];
277         } else if (tokens[0] === 'ANNOTATION') {
278             let position = parser.parse_yx(tokens[1]);
279             explorer.update_info_db(position, tokens[2]);
280             tui.restore_input_values();
281             tui.full_refresh();
282         } else if (tokens[0] === 'UNHANDLED_INPUT') {
283             tui.log_msg('? unknown command');
284         } else if (tokens[0] === 'PLAY_ERROR') {
285             tui.log_msg('? ' + tokens[1]);
286             terminal.blink_screen();
287         } else if (tokens[0] === 'ARGUMENT_ERROR') {
288             tui.log_msg('? syntax error: ' + tokens[1]);
289         } else if (tokens[0] === 'GAME_ERROR') {
290             tui.log_msg('? game error: ' + tokens[1]);
291         } else if (tokens[0] === 'PONG') {
292             ;
293         } else {
294             tui.log_msg('? unhandled input: ' + event.data);
295         }
296     }
297 }
298
299 let unparser = {
300     quote: function(str) {
301         let quoted = ['"'];
302         for (let i = 0; i < str.length; i++) {
303             let c = str[i];
304             if (['"', '\\'].includes(c)) {
305                 quoted.push('\\');
306             };
307             quoted.push(c);
308         }
309         quoted.push('"');
310         return quoted.join('');
311     },
312     to_yx: function(yx_coordinate) {
313         return "Y:" + yx_coordinate[0] + ",X:" + yx_coordinate[1];
314     },
315     untokenize: function(tokens) {
316         let quoted_tokens = [];
317         for (let token of tokens) {
318             quoted_tokens.push(this.quote(token));
319         }
320         return quoted_tokens.join(" ");
321     }
322 }
323
324 class Mode {
325     constructor(name, help_intro, has_input_prompt=false, shows_info=false, is_intro=false) {
326         this.name = name;
327         this.has_input_prompt = has_input_prompt;
328         this.shows_info= shows_info;
329         this.is_intro = is_intro;
330         this.help_intro = help_intro;
331     }
332 }
333 let mode_waiting_for_server = new Mode('waiting_for_server', 'Waiting for a server response.', false, false, true);
334 let mode_login = new Mode('login', 'Pick your player name.', true, false, true);
335 let mode_post_login_wait = new Mode('waiting for game world', 'Waiting for a server response.', false, false, true);
336 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);
337   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);
338 let mode_play = new Mode('play', 'This mode allows you to interact with the map.', false, false);
339 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);
340 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);
341 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);
342 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);
343
344 let tui = {
345   mode: mode_waiting_for_server,
346   log: [],
347   input_prompt: '> ',
348   input_lines: [],
349   window_width: terminal.cols / 2,
350   height_turn_line: 1,
351   height_mode_line: 1,
352   height_input: 1,
353   password: 'foo',
354   show_help: false,
355   init: function() {
356       this.inputEl = document.getElementById("input");
357       this.inputEl.focus();
358       this.recalc_input_lines();
359       this.height_header = this.height_turn_line + this.height_mode_line;
360       this.log_msg("@ waiting for server connection ...");
361       this.init_keys();
362   },
363   init_keys: function() {
364     this.keys = {};
365     for (let key_selector of key_selectors) {
366         this.keys[key_selector.id.slice(4)] = key_selector.value;
367     }
368     this.movement_keys = {
369         [this.keys.square_move_up]: 'UP',
370         [this.keys.square_move_left]: 'LEFT',
371         [this.keys.square_move_down]: 'DOWN',
372         [this.keys.square_move_right]: 'RIGHT'
373     };
374     if (game.map_geometry == 'Hex') {
375         this.movement_keys = {
376             [this.keys.hex_move_upleft]: 'UPLEFT',
377             [this.keys.hex_move_upright]: 'UPRIGHT',
378             [this.keys.hex_move_right]: 'RIGHT',
379             [this.keys.hex_move_downright]: 'DOWNRIGHT',
380             [this.keys.hex_move_downleft]: 'DOWNLEFT',
381             [this.keys.hex_move_left]: 'LEFT'
382         };
383     };
384   },
385   switch_mode: function(mode) {
386     this.inputEl.focus();
387     this.show_help = false;
388     this.map_mode = 'terrain';
389     if (mode.shows_info && game.player_id in game.things) {
390       explorer.position = game.things[game.player_id].position;
391       explorer.query_info();
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 += '/' + this.keys.switch_to_play + ' or /play – switch to play mode\n';
665           content += '/' + this.keys.switch_to_study + ' or /study – switch to study mode\n';
666       }
667       let start_x = 0;
668       if (!this.mode.has_input_prompt) {
669           start_x = this.window_width
670       }
671       terminal.drawBox(0, start_x, terminal.rows, this.window_width);
672       let lines = this.msg_into_lines_of_width(content, this.window_width);
673       for (let y = 0, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
674           terminal.write(y, start_x, lines[i]);
675       }
676   },
677   full_refresh: function() {
678     terminal.drawBox(0, 0, terminal.rows, terminal.cols);
679     if (this.mode.is_intro) {
680         this.draw_history();
681         this.draw_input();
682     } else {
683         if (game.turn_complete) {
684             this.draw_map();
685             this.draw_turn_line();
686         }
687         this.draw_mode_line();
688         if (this.mode.shows_info) {
689           this.draw_info();
690         } else {
691           this.draw_history();
692         }
693         this.draw_input();
694     }
695     if (this.show_help) {
696         this.draw_help();
697     }
698     terminal.refresh();
699   }
700 }
701
702 let game = {
703     init: function() {
704         this.things = {};
705         this.turn = -1;
706         this.map = "";
707         this.map_control = "";
708         this.map_size = [0,0];
709         this.player_id = -1;
710         this.portals = {};
711         this.tasks = {};
712     },
713     get_thing: function(id_, create_if_not_found=false) {
714         if (id_ in game.things) {
715             return game.things[id_];
716         } else if (create_if_not_found) {
717             let t = new Thing([0,0]);
718             game.things[id_] = t;
719             return t;
720         };
721     },
722     move: function(start_position, direction) {
723         let target = [start_position[0], start_position[1]];
724         if (direction == 'LEFT') {
725             target[1] -= 1;
726         } else if (direction == 'RIGHT') {
727             target[1] += 1;
728         } else if (game.map_geometry == 'Square') {
729             if (direction == 'UP') {
730                 target[0] -= 1;
731             } else if (direction == 'DOWN') {
732                 target[0] += 1;
733             };
734         } else if (game.map_geometry == 'Hex') {
735             let start_indented = start_position[0] % 2;
736             if (direction == 'UPLEFT') {
737                 target[0] -= 1;
738                 if (!start_indented) {
739                     target[1] -= 1;
740                 }
741             } else if (direction == 'UPRIGHT') {
742                 target[0] -= 1;
743                 if (start_indented) {
744                     target[1] += 1;
745                 }
746             } else if (direction == 'DOWNLEFT') {
747                 target[0] += 1;
748                 if (!start_indented) {
749                     target[1] -= 1;
750                 }
751             } else if (direction == 'DOWNRIGHT') {
752                 target[0] += 1;
753                 if (start_indented) {
754                     target[1] += 1;
755                 }
756             };
757         };
758         if (target[0] < 0 || target[1] < 0 ||
759             target[0] >= this.map_size[0] || target[1] >= this.map_size[1]) {
760             return null;
761         };
762         return target;
763     },
764     teleport: function() {
765         let player = this.get_thing(game.player_id);
766         if (player.position in this.portals) {
767             server.reconnect_to(this.portals[player.position]);
768         } else {
769             terminal.blink_screen();
770             tui.log_msg('? not standing on portal')
771         }
772     }
773 }
774
775 game.init();
776 tui.init();
777 tui.full_refresh();
778 server.init(websocket_location);
779
780 let explorer = {
781     position: [0,0],
782     info_db: {},
783     move: function(direction) {
784         let target = game.move(this.position, direction);
785         if (target) {
786             this.position = target
787             this.query_info();
788         } else {
789             terminal.blink_screen();
790         };
791     },
792     update_info_db: function(yx, str) {
793         this.info_db[yx] = str;
794         if (tui.mode == mode_study) {
795             tui.full_refresh();
796         }
797     },
798     empty_info_db: function() {
799         this.info_db = {};
800         if (tui.mode == mode_study) {
801             tui.full_refresh();
802         }
803     },
804     query_info: function() {
805         server.send(["GET_ANNOTATION", unparser.to_yx(explorer.position)]);
806     },
807     get_info: function() {
808         let position_i = this.position[0] * game.map_size[1] + this.position[1];
809         if (game.fov[position_i] != '.') {
810             return 'outside field of view';
811         };
812         let info = "";
813         let terrain_char = game.map[position_i]
814         let terrain_desc = '?'
815         if (game.terrains[terrain_char]) {
816             terrain_desc = game.terrains[terrain_char];
817         };
818         info += 'TERRAIN: "' + terrain_char + '" / ' + terrain_desc + "\n";
819         for (let t_id in game.things) {
820              let t = game.things[t_id];
821              if (t.position[0] == this.position[0] && t.position[1] == this.position[1]) {
822                  let symbol = game.thing_types[t.type_];
823                  info += "THING: " + t.type_ + " / " + symbol;
824                  if (t.player_char) {
825                      info += t.player_char;
826                  };
827                  if (t.name_) {
828                      info += " (" + t.name_ + ")";
829                  }
830                  info += "\n";
831              }
832         }
833         if (this.position in game.portals) {
834             info += "PORTAL: " + game.portals[this.position] + "\n";
835         }
836         if (this.position in this.info_db) {
837             info += "ANNOTATIONS: " + this.info_db[this.position];
838         } else {
839             info += 'waiting …';
840         }
841         return info;
842     },
843     annotate: function(msg) {
844         if (msg.length == 0) {
845             msg = " ";  // triggers annotation deletion
846         }
847         server.send(["ANNOTATE", unparser.to_yx(explorer.position), msg, tui.password]);
848     },
849     set_portal: function(msg) {
850         if (msg.length == 0) {
851             msg = " ";  // triggers portal deletion
852         }
853         server.send(["PORTAL", unparser.to_yx(explorer.position), msg, tui.password]);
854     }
855 }
856
857 tui.inputEl.addEventListener('input', (event) => {
858     if (tui.mode.has_input_prompt) {
859         let max_length = tui.window_width * terminal.rows - tui.input_prompt.length;
860         if (tui.inputEl.value.length > max_length) {
861             tui.inputEl.value = tui.inputEl.value.slice(0, max_length);
862         };
863         tui.recalc_input_lines();
864     } else if (tui.mode == mode_edit && tui.inputEl.value.length > 0) {
865         server.send(["TASK:WRITE", tui.inputEl.value[0], tui.password]);
866         tui.switch_mode(mode_play);
867     }
868     tui.full_refresh();
869 }, false);
870 tui.inputEl.addEventListener('keydown', (event) => {
871     tui.show_help = false;
872     if (event.key == 'Enter') {
873         event.preventDefault();
874     }
875     if (tui.mode.has_input_prompt && event.key == 'Enter' && tui.inputEl.value == '/help') {
876         tui.show_help = true;
877         tui.empty_input();
878         tui.restore_input_values();
879     } else if (!tui.mode.has_input_prompt && event.key == tui.keys.help) {
880         tui.show_help = true;
881     } else if (tui.mode == mode_login && event.key == 'Enter') {
882         tui.login_name = tui.inputEl.value;
883         server.send(['LOGIN', tui.inputEl.value]);
884         tui.empty_input();
885     } else if (tui.mode == mode_portal && event.key == 'Enter') {
886         explorer.set_portal(tui.inputEl.value);
887         tui.switch_mode(mode_play);
888     } else if (tui.mode == mode_annotate && event.key == 'Enter') {
889         explorer.annotate(tui.inputEl.value);
890         tui.switch_mode(mode_play);
891     } else if (tui.mode == mode_password && event.key == 'Enter') {
892         if (tui.inputEl.value.length == 0) {
893             tui.inputEl.value = " ";
894         }
895         tui.password = tui.inputEl.value
896         tui.switch_mode(mode_play);
897     } else if (tui.mode == mode_chat && event.key == 'Enter') {
898         let tokens = parser.tokenize(tui.inputEl.value);
899         if (tokens.length > 0 && tokens[0].length > 0) {
900             if (tui.inputEl.value[0][0] == '/') {
901                 if (tokens[0].slice(1) == 'play' || tokens[0][1] == tui.keys.switch_to_play) {
902                     tui.switch_mode(mode_play);
903                 } else if (tokens[0].slice(1) == 'study' || tokens[0][1] == tui.keys.switch_to_study) {
904                     tui.switch_mode(mode_study);
905                 } else if (tokens[0].slice(1) == 'nick') {
906                     if (tokens.length > 1) {
907                         server.send(['NICK', tokens[1]]);
908                     } else {
909                         tui.log_msg('? need new name');
910                     }
911                 } else {
912                     tui.log_msg('? unknown command');
913                 }
914             } else {
915                     server.send(['ALL', tui.inputEl.value]);
916             }
917         } else if (tui.inputEl.valuelength > 0) {
918                 server.send(['ALL', tui.inputEl.value]);
919         }
920         tui.empty_input();
921     } else if (tui.mode == mode_play) {
922           if (event.key === tui.keys.switch_to_chat) {
923               event.preventDefault();
924               tui.switch_mode(mode_chat);
925           } else if (event.key === tui.keys.switch_to_edit
926                      && game.tasks.includes('WRITE')) {
927               event.preventDefault();
928               tui.switch_mode(mode_edit);
929           } else if (event.key === tui.keys.switch_to_study) {
930               tui.switch_mode(mode_study);
931           } else if (event.key === tui.keys.switch_to_password) {
932               event.preventDefault();
933               tui.switch_mode(mode_password);
934           } else if (event.key === tui.keys.flatten
935                      && game.tasks.includes('FLATTEN_SURROUNDINGS')) {
936               server.send(["TASK:FLATTEN_SURROUNDINGS", tui.password]);
937           } else if (event.key === tui.keys.take_thing
938                      && game.tasks.includes('PICK_UP')) {
939               server.send(["TASK:PICK_UP"]);
940           } else if (event.key === tui.keys.drop_thing
941                      && game.tasks.includes('DROP')) {
942               server.send(["TASK:DROP"]);
943           } else if (event.key in tui.movement_keys
944                      && game.tasks.includes('MOVE')) {
945               server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
946           } else if (event.key === tui.keys.teleport) {
947               game.teleport();
948           } else if (event.key === tui.keys.switch_to_portal) {
949               event.preventDefault();
950               tui.switch_mode(mode_portal);
951           } else if (event.key === tui.keys.switch_to_annotate) {
952               event.preventDefault();
953               tui.switch_mode(mode_annotate);
954           };
955     } else if (tui.mode == mode_study) {
956         if (event.key === tui.keys.switch_to_chat) {
957             event.preventDefault();
958             tui.switch_mode(mode_chat);
959         } else if (event.key == tui.keys.switch_to_play) {
960             tui.switch_mode(mode_play);
961         } else if (event.key in tui.movement_keys) {
962             explorer.move(tui.movement_keys[event.key]);
963         } else if (event.key == tui.keys.toggle_map_mode) {
964             if (tui.map_mode == 'terrain') {
965                 tui.map_mode = 'control';
966             } else {
967                 tui.map_mode = 'terrain';
968             }
969         };
970     }
971     tui.full_refresh();
972 }, false);
973
974 rows_selector.addEventListener('input', function() {
975     if (rows_selector.value % 4 != 0 || rows_selector.value < 24) {
976         return;
977     }
978     window.localStorage.setItem(rows_selector.id, rows_selector.value);
979     terminal.initialize();
980     tui.full_refresh();
981 }, false);
982 cols_selector.addEventListener('input', function() {
983     if (cols_selector.value % 4 != 0 || cols_selector.value < 80) {
984         return;
985     }
986     window.localStorage.setItem(cols_selector.id, cols_selector.value);
987     terminal.initialize();
988     tui.window_width = terminal.cols / 2,
989     tui.full_refresh();
990 }, false);
991 for (let key_selector of key_selectors) {
992     key_selector.addEventListener('input', function() {
993         window.localStorage.setItem(key_selector.id, key_selector.value);
994         tui.init_keys();
995     }, false);
996 }
997 window.setInterval(function() {
998     if (server.connected) {
999         server.send(['PING']);
1000     } else {
1001         server.reconnect_to(server.url);
1002         tui.log_msg('@ attempting reconnect …')
1003     }
1004 }, 5000);
1005 document.getElementById("terminal").onclick = function() {
1006     tui.inputEl.focus();
1007 };
1008 document.getElementById("help").onclick = function() {
1009     tui.show_help = true;
1010     tui.full_refresh();
1011 };
1012 document.getElementById("switch_to_play").onclick = function() {
1013     tui.switch_mode(mode_play);
1014     tui.full_refresh();
1015 };
1016 document.getElementById("switch_to_study").onclick = function() {
1017     tui.switch_mode(mode_study);
1018     tui.full_refresh();
1019 };
1020 document.getElementById("switch_to_chat").onclick = function() {
1021     tui.switch_mode(mode_chat);
1022     tui.full_refresh();
1023 };
1024 document.getElementById("switch_to_password").onclick = function() {
1025     tui.switch_mode(mode_password);
1026     tui.full_refresh();
1027 };
1028 document.getElementById("switch_to_edit").onclick = function() {
1029     tui.switch_mode(mode_edit);
1030     tui.full_refresh();
1031 };
1032 document.getElementById("switch_to_annotate").onclick = function() {
1033     tui.switch_mode(mode_annotate);
1034     tui.full_refresh();
1035 };
1036 document.getElementById("switch_to_portal").onclick = function() {
1037     tui.switch_mode(mode_portal);
1038     tui.full_refresh();
1039 };
1040 document.getElementById("toggle_map_mode").onclick = function() {
1041     if (tui.map_mode == 'terrain') {
1042         tui.map_mode = 'control';
1043     } else {
1044         tui.map_mode = 'terrain';
1045     }
1046     tui.full_refresh();
1047 };
1048 document.getElementById("take_thing").onclick = function() {
1049         server.send(['TASK:PICK_UP']);
1050 };
1051 document.getElementById("drop_thing").onclick = function() {
1052         server.send(['TASK:DROP']);
1053 };
1054 document.getElementById("flatten").onclick = function() {
1055     server.send(['TASK:FLATTEN_SURROUNDINGS', tui.password]);
1056 };
1057 document.getElementById("teleport").onclick = function() {
1058     game.teleport();
1059 };
1060 document.getElementById("move_upleft").onclick = function() {
1061     if (tui.mode == mode_play) {
1062         server.send(['TASK:MOVE', 'UPLEFT']);
1063     } else {
1064         explorer.move('UPLEFT');
1065     };
1066 };
1067 document.getElementById("move_left").onclick = function() {
1068     if (tui.mode == mode_play) {
1069         server.send(['TASK:MOVE', 'LEFT']);
1070     } else {
1071         explorer.move('LEFT');
1072     };
1073 };
1074 document.getElementById("move_downleft").onclick = function() {
1075     if (tui.mode == mode_play) {
1076         server.send(['TASK:MOVE', 'DOWNLEFT']);
1077     } else {
1078         explorer.move('DOWNLEFT');
1079     };
1080 };
1081 document.getElementById("move_down").onclick = function() {
1082     if (tui.mode == mode_play) {
1083         server.send(['TASK:MOVE', 'DOWN']);
1084     } else {
1085         explorer.move('DOWN');
1086     };
1087 };
1088 document.getElementById("move_up").onclick = function() {
1089     if (tui.mode == mode_play) {
1090         server.send(['TASK:MOVE', 'UP']);
1091     } else {
1092         explorer.move('UP');
1093     };
1094 };
1095 document.getElementById("move_upright").onclick = function() {
1096     if (tui.mode == mode_play) {
1097         server.send(['TASK:MOVE', 'UPRIGHT']);
1098     } else {
1099         explorer.move('UPRIGHT');
1100     };
1101 };
1102 document.getElementById("move_right").onclick = function() {
1103     if (tui.mode == mode_play) {
1104         server.send(['TASK:MOVE', 'RIGHT']);
1105     } else {
1106         explorer.move('RIGHT');
1107     };
1108 };
1109 document.getElementById("move_downright").onclick = function() {
1110     if (tui.mode == mode_play) {
1111         server.send(['TASK:MOVE', 'DOWNRIGHT']);
1112     } else {
1113         explorer.move('DOWNRIGHT');
1114     };
1115 };
1116 </script>
1117 </body></html>