home · contact · privacy
b8d7b986d11c1350bf203f5a8f380bc4b1298816
[plomrogue2] / rogue_chat_nocanvas_monochrome.html
1 <!DOCTYPE html>
2 <html><head>
3 <style>
4 </style>
5 </head><body>
6 <div>
7 terminal rows: <input id="n_rows" type="number" step=4 min=24 value=24 />
8 terminal columns: <input id="n_cols" type="number" step=4 min=80 value=80 />
9 </div>
10 <pre id="terminal" style="display: inline-block;"></pre>
11 <textarea id="input" style="opacity: 0; width: 0px;"></textarea>
12 <div>
13 <h3>for mouse players</h3>
14 <table style="float: left">
15 <tr><td><button id="move_upleft">up-left</button></td><td><button id="move_up">up</button></td><td><button id="move_upright">up-right</button></td></tr>
16 <tr><td><button id="move_left">left</button></td><td>MOVE</td><td><button id="move_right">right</button></td></tr>
17 <tr><td><button id="move_downleft">down-left</button></td><td><button id="move_down">down</button></td><td><button id="move_downright">down-right</button></td></tr>
18 </table>
19 <div>
20 <button id="help">help</button>
21 <button id="switch_to_play">play mode</button>
22 <button id="switch_to_study">study mode</button>
23 <button id="switch_to_chat">chat mode</button><br />
24 <button id="take_thing">take thing</button>
25 <button id="drop_thing">drop thing</button>
26 <button id="flatten">flatten surroundings</button>
27 <button id="teleport">teleport</button>
28 <button id="switch_to_edit">change tile</button><br />
29 <button id="switch_to_password">change tile editing password</button>
30 <button id="switch_to_annotate">annotate tile</button>
31 <button id="switch_to_portal">edit portal link</button>
32 <button id="toggle_map_mode">toggle terrain/annotations/control view</button>
33 </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/annotations/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             explorer.empty_info_db();
227             game.things = {};
228             game.portals = {};
229             game.turn = parseInt(tokens[1]);
230         } else if (tokens[0] === 'THING') {
231             let t = game.get_thing(tokens[3], true);
232             t.position = parser.parse_yx(tokens[1]);
233             t.type_ = tokens[2];
234         } else if (tokens[0] === 'THING_NAME') {
235             let t = game.get_thing(tokens[1], false);
236             if (t) {
237                 t.name_ = tokens[2];
238             };
239         } else if (tokens[0] === 'THING_CHAR') {
240             let t = game.get_thing(tokens[1], false);
241             if (t) {
242                 t.player_char = tokens[2];
243             };
244         } else if (tokens[0] === 'TASKS') {
245             game.tasks = tokens[1].split(',')
246         } else if (tokens[0] === 'THING_TYPE') {
247             game.thing_types[tokens[1]] = tokens[2]
248         } else if (tokens[0] === 'TERRAIN') {
249             game.terrains[tokens[1]] = tokens[2]
250         } else if (tokens[0] === 'MAP') {
251             game.map_geometry = tokens[1];
252             tui.init_keys();
253             game.map_size = parser.parse_yx(tokens[2]);
254             game.map = tokens[3]
255         } else if (tokens[0] === 'FOV') {
256             game.fov = tokens[1]
257         } else if (tokens[0] === 'MAP_CONTROL') {
258             game.map_control = tokens[1]
259         } else if (tokens[0] === 'GAME_STATE_COMPLETE') {
260             game.turn_complete = true;
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_HINT') {
278             let position = parser.parse_yx(tokens[1]);
279             explorer.info_hints = explorer.info_hints.concat([position]);
280         } else if (tokens[0] === 'ANNOTATION') {
281             let position = parser.parse_yx(tokens[1]);
282             explorer.update_info_db(position, tokens[2]);
283             tui.restore_input_values();
284             tui.full_refresh();
285         } else if (tokens[0] === 'UNHANDLED_INPUT') {
286             tui.log_msg('? unknown command');
287         } else if (tokens[0] === 'PLAY_ERROR') {
288             tui.log_msg('? ' + tokens[1]);
289             terminal.blink_screen();
290         } else if (tokens[0] === 'ARGUMENT_ERROR') {
291             tui.log_msg('? syntax error: ' + tokens[1]);
292         } else if (tokens[0] === 'GAME_ERROR') {
293             tui.log_msg('? game error: ' + tokens[1]);
294         } else if (tokens[0] === 'PONG') {
295             ;
296         } else {
297             tui.log_msg('? unhandled input: ' + event.data);
298         }
299     }
300 }
301
302 let unparser = {
303     quote: function(str) {
304         let quoted = ['"'];
305         for (let i = 0; i < str.length; i++) {
306             let c = str[i];
307             if (['"', '\\'].includes(c)) {
308                 quoted.push('\\');
309             };
310             quoted.push(c);
311         }
312         quoted.push('"');
313         return quoted.join('');
314     },
315     to_yx: function(yx_coordinate) {
316         return "Y:" + yx_coordinate[0] + ",X:" + yx_coordinate[1];
317     },
318     untokenize: function(tokens) {
319         let quoted_tokens = [];
320         for (let token of tokens) {
321             quoted_tokens.push(this.quote(token));
322         }
323         return quoted_tokens.join(" ");
324     }
325 }
326
327 class Mode {
328     constructor(name, help_intro, has_input_prompt=false, shows_info=false, is_intro=false) {
329         this.name = name;
330         this.has_input_prompt = has_input_prompt;
331         this.shows_info= shows_info;
332         this.is_intro = is_intro;
333         this.help_intro = help_intro;
334     }
335 }
336 let mode_waiting_for_server = new Mode('waiting_for_server', 'Waiting for a server response.', false, false, true);
337 let mode_login = new Mode('login', 'Pick your player name.', true, false, true);
338 let mode_post_login_wait = new Mode('waiting for game world', 'Waiting for a server response.', false, false, true);
339 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);
340   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);
341 let mode_play = new Mode('play', 'This mode allows you to interact with the map.', false, false);
342 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);
343 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);
344 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);
345 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);
346
347 let tui = {
348   mode: mode_waiting_for_server,
349   log: [],
350   input_prompt: '> ',
351   input_lines: [],
352   window_width: terminal.cols / 2,
353   height_turn_line: 1,
354   height_mode_line: 1,
355   height_input: 1,
356   password: 'foo',
357   show_help: false,
358   init: function() {
359       this.inputEl = document.getElementById("input");
360       this.inputEl.focus();
361       this.recalc_input_lines();
362       this.height_header = this.height_turn_line + this.height_mode_line;
363       this.log_msg("@ waiting for server connection ...");
364       this.init_keys();
365   },
366   init_keys: function() {
367     this.keys = {};
368     for (let key_selector of key_selectors) {
369         this.keys[key_selector.id.slice(4)] = key_selector.value;
370     }
371     this.movement_keys = {
372         [this.keys.square_move_up]: 'UP',
373         [this.keys.square_move_left]: 'LEFT',
374         [this.keys.square_move_down]: 'DOWN',
375         [this.keys.square_move_right]: 'RIGHT'
376     };
377     if (game.map_geometry == 'Hex') {
378         this.movement_keys = {
379             [this.keys.hex_move_upleft]: 'UPLEFT',
380             [this.keys.hex_move_upright]: 'UPRIGHT',
381             [this.keys.hex_move_right]: 'RIGHT',
382             [this.keys.hex_move_downright]: 'DOWNRIGHT',
383             [this.keys.hex_move_downleft]: 'DOWNLEFT',
384             [this.keys.hex_move_left]: 'LEFT'
385         };
386     };
387   },
388   switch_mode: function(mode) {
389     this.inputEl.focus();
390     this.show_help = false;
391     this.map_mode = 'terrain';
392     if (mode.shows_info && game.player_id in game.things) {
393       explorer.position = game.things[game.player_id].position;
394       explorer.query_info();
395     }
396     this.mode = mode;
397     this.empty_input();
398     this.restore_input_values();
399     document.getElementById("take_thing").disabled = true;
400     document.getElementById("drop_thing").disabled = true;
401     document.getElementById("flatten").disabled = true;
402     document.getElementById("teleport").disabled = true;
403     document.getElementById("toggle_map_mode").disabled = true;
404     document.getElementById("switch_to_chat").disabled = true;
405     document.getElementById("switch_to_play").disabled = true;
406     document.getElementById("switch_to_study").disabled = true;
407     document.getElementById("switch_to_edit").disabled = true;
408     document.getElementById("switch_to_portal").disabled = true;
409     document.getElementById("switch_to_annotate").disabled = true;
410     document.getElementById("switch_to_password").disabled = true;
411     document.getElementById("move_left").disabled = true;
412     document.getElementById("move_upleft").disabled = true;
413     document.getElementById("move_up").disabled = true;
414     document.getElementById("move_upright").disabled = true;
415     document.getElementById("move_downleft").disabled = true;
416     document.getElementById("move_down").disabled = true;
417     document.getElementById("move_downright").disabled = true;
418     document.getElementById("move_right").disabled = true;
419     if (mode == mode_play || mode == mode_study) {
420         document.getElementById("move_left").disabled = false;
421         document.getElementById("move_right").disabled = false;
422         if (game.map_geometry == 'Hex') {
423             document.getElementById("move_upleft").disabled = false;
424             document.getElementById("move_upright").disabled = false;
425             document.getElementById("move_downleft").disabled = false;
426             document.getElementById("move_downright").disabled = false;
427         } else {
428             document.getElementById("move_up").disabled = false;
429             document.getElementById("move_down").disabled = false;
430         }
431     }
432     if (!mode.is_intro && mode != mode_play) {
433         document.getElementById("switch_to_play").disabled = false;
434     }
435     if (!mode.is_intro && mode != mode_study) {
436         document.getElementById("switch_to_study").disabled = false;
437     }
438     if (!mode.is_intro && mode != mode_chat) {
439         document.getElementById("switch_to_chat").disabled = false;
440     }
441     if (mode == mode_login) {
442         if (this.login_name) {
443             server.send(['LOGIN', this.login_name]);
444         } else {
445             this.log_msg("? need login name");
446         }
447     } else if (mode == mode_play) {
448         if (game.tasks.includes('PICK_UP')) {
449             document.getElementById("take_thing").disabled = false;
450         }
451         if (game.tasks.includes('DROP')) {
452             document.getElementById("drop_thing").disabled = false;
453         }
454         if (game.tasks.includes('FLATTEN_SURROUNDINGS')) {
455             document.getElementById("flatten").disabled = false;
456         }
457         if (game.tasks.includes('MOVE')) {
458         }
459         document.getElementById("teleport").disabled = false;
460         document.getElementById("switch_to_annotate").disabled = false;
461         document.getElementById("switch_to_edit").disabled = false;
462         document.getElementById("switch_to_portal").disabled = false;
463         document.getElementById("switch_to_password").disabled = false;
464     } else if (mode == mode_study) {
465         document.getElementById("toggle_map_mode").disabled = false;
466     } else if (mode == mode_edit) {
467         this.show_help = true;
468     }
469     this.full_refresh();
470   },
471   restore_input_values: function() {
472       if (this.mode == mode_annotate && explorer.position in explorer.info_db) {
473           let info = explorer.info_db[explorer.position];
474           if (info != "(none)") {
475               this.inputEl.value = info;
476               this.recalc_input_lines();
477           }
478       } else if (this.mode == mode_portal && explorer.position in game.portals) {
479           let portal = game.portals[explorer.position]
480           this.inputEl.value = portal;
481           this.recalc_input_lines();
482       } else if (this.mode == mode_password) {
483           this.inputEl.value = this.password;
484           this.recalc_input_lines();
485       }
486   },
487   empty_input: function(str) {
488       this.inputEl.value = "";
489       if (this.mode.has_input_prompt) {
490           this.recalc_input_lines();
491       } else {
492           this.height_input = 0;
493       }
494   },
495   recalc_input_lines: function() {
496       this.input_lines = this.msg_into_lines_of_width(this.input_prompt + this.inputEl.value, this.window_width);
497       this.height_input = this.input_lines.length;
498   },
499   msg_into_lines_of_width: function(msg, width) {
500     let chunk = "";
501     let lines = [];
502     for (let i = 0, x = 0; i < msg.length; i++, x++) {
503       if (x >= width || msg[i] == "\n") {
504         lines.push(chunk);
505         chunk = "";
506         x = 0;
507       };
508       if (msg[i] != "\n") {
509         chunk += msg[i];
510       }
511     }
512     lines.push(chunk);
513     return lines;
514   },
515   log_msg: function(msg) {
516       this.log.push(msg);
517       while (this.log.length > 100) {
518         this.log.shift();
519       };
520       this.full_refresh();
521   },
522   draw_map: function() {
523     let map_lines_split = [];
524     let line = [];
525     let map_content = game.map;
526     if (this.map_mode == 'control') {
527         map_content = game.map_control;
528     }
529     for (let i = 0, j = 0; i < game.map.length; i++, j++) {
530         if (j == game.map_size[1]) {
531             map_lines_split.push(line);
532             line = [];
533             j = 0;
534         };
535         line.push(map_content[i] + ' ');
536     };
537     map_lines_split.push(line);
538     if (this.map_mode == 'annotations') {
539         for (const coordinate of explorer.info_hints) {
540             map_lines_split[coordinate[0]][coordinate[1]] = 'A ';
541         }
542     } else if (this.map_mode == 'terrain') {
543         for (const p in game.portals) {
544             let coordinate = p.split(',')
545             map_lines_split[coordinate[0]][coordinate[1]] = 'P ';
546         }
547         let used_positions = [];
548         for (const thing_id in game.things) {
549             let t = game.things[thing_id];
550             let symbol = game.thing_types[t.type_];
551             let meta_char = ' ';
552             if (t.player_char) {
553                 meta_char = t.player_char;
554             }
555             if (used_positions.includes(t.position.toString())) {
556                 meta_char = '+';
557             };
558             map_lines_split[t.position[0]][t.position[1]] = symbol + meta_char;
559             used_positions.push(t.position.toString());
560         };
561     }
562     if (tui.mode.shows_info) {
563         map_lines_split[explorer.position[0]][explorer.position[1]] = '??';
564     }
565     let map_lines = []
566     if (game.map_geometry == 'Square') {
567         for (let line_split of map_lines_split) {
568             map_lines.push(line_split.join(''));
569         };
570     } else if (game.map_geometry == 'Hex') {
571         let indent = 0
572         for (let line_split of map_lines_split) {
573             map_lines.push(' '.repeat(indent) + line_split.join(''));
574             if (indent == 0) {
575                 indent = 1;
576             } else {
577                 indent = 0;
578             };
579         };
580     }
581     let window_center = [terminal.rows / 2, this.window_width / 2];
582     let player = game.things[game.player_id];
583     let center_position = [player.position[0], player.position[1]];
584     if (tui.mode.shows_info) {
585         center_position = [explorer.position[0], explorer.position[1]];
586     }
587     center_position[1] = center_position[1] * 2;
588     let offset = [center_position[0] - window_center[0],
589                   center_position[1] - window_center[1]]
590     if (game.map_geometry == 'Hex' && offset[0] % 2) {
591         offset[1] += 1;
592     };
593     let term_y = Math.max(0, -offset[0]);
594     let term_x = Math.max(0, -offset[1]);
595     let map_y = Math.max(0, offset[0]);
596     let map_x = Math.max(0, offset[1]);
597     for (; term_y < terminal.rows && map_y < game.map_size[0]; term_y++, map_y++) {
598         let to_draw = map_lines[map_y].slice(map_x, this.window_width + offset[1]);
599         terminal.write(term_y, term_x, to_draw);
600     }
601   },
602   draw_mode_line: function() {
603       let help = 'hit [' + this.keys.help + '] for help';
604       if (this.mode.has_input_prompt) {
605           help = 'enter /help for help';
606       }
607       terminal.write(0, this.window_width, 'MODE: ' + this.mode.name + ' – ' + help);
608   },
609   draw_turn_line: function(n) {
610     terminal.write(1, this.window_width, 'TURN: ' + game.turn);
611   },
612   draw_history: function() {
613       let log_display_lines = [];
614       for (let line of this.log) {
615           log_display_lines = log_display_lines.concat(this.msg_into_lines_of_width(line, this.window_width));
616       };
617       for (let y = terminal.rows - 1 - this.height_input,
618                i = log_display_lines.length - 1;
619            y >= this.height_header && i >= 0;
620            y--, i--) {
621           terminal.write(y, this.window_width, log_display_lines[i]);
622       }
623   },
624   draw_info: function() {
625     let lines = this.msg_into_lines_of_width(explorer.get_info(), this.window_width);
626     for (let y = this.height_header, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
627       terminal.write(y, this.window_width, lines[i]);
628     }
629   },
630   draw_input: function() {
631     if (this.mode.has_input_prompt) {
632         for (let y = terminal.rows - this.height_input, i = 0; i < this.input_lines.length; y++, i++) {
633             terminal.write(y, this.window_width, this.input_lines[i]);
634         }
635     }
636   },
637   draw_help: function() {
638       let movement_keys_desc = Object.keys(this.movement_keys).join(',');
639       let content = this.mode.name + " mode help\n\n" + this.mode.help_intro + "\n\n";
640       if (this.mode == mode_play) {
641           content += "Available actions:\n";
642           if (game.tasks.includes('MOVE')) {
643               content += "[" + movement_keys_desc + "] – move player\n";
644           }
645           if (game.tasks.includes('PICK_UP')) {
646               content += "[" + this.keys.take_thing + "] – take thing under player\n";
647           }
648           if (game.tasks.includes('DROP')) {
649               content += "[" + this.keys.drop_thing + "] – drop carried thing\n";
650           }
651           if (game.tasks.includes('FLATTEN_SURROUNDINGS')) {
652               content += "[" + tui.keys.flatten + "] – flatten player's surroundings\n";
653           }
654           content += "[" + tui.keys.teleport + "] – teleport to other space\n";
655           content += '\nOther modes available from here:\n';
656           content += '[' + this.keys.switch_to_chat + '] – chat mode\n';
657           content += '[' + this.keys.switch_to_study + '] – study mode\n';
658           content += '[' + this.keys.switch_to_edit + '] – terrain edit mode\n';
659           content += '[' + this.keys.switch_to_portal + '] – portal edit mode\n';
660           content += '[' + this.keys.switch_to_annotate + '] – annotation mode\n';
661           content += '[' + this.keys.switch_to_password + '] – password input mode\n';
662       } else if (this.mode == mode_study) {
663           content += "Available actions:\n";
664           content += '[' + movement_keys_desc + '] – move question mark\n';
665           content += '[' + this.keys.toggle_map_mode + '] – toggle view between terrain, annotations, and password protection areas\n';
666           content += '\nOther modes available from here:\n';
667           content += '[' + this.keys.switch_to_chat + '] – chat mode\n';
668           content += '[' + this.keys.switch_to_play + '] – play mode\n';
669       } else if (this.mode == mode_chat) {
670           content += '/nick NAME – re-name yourself to NAME\n';
671           content += '/' + this.keys.switch_to_play + ' or /play – switch to play mode\n';
672           content += '/' + this.keys.switch_to_study + ' or /study – switch to study mode\n';
673       }
674       let start_x = 0;
675       if (!this.mode.has_input_prompt) {
676           start_x = this.window_width
677       }
678       terminal.drawBox(0, start_x, terminal.rows, this.window_width);
679       let lines = this.msg_into_lines_of_width(content, this.window_width);
680       for (let y = 0, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
681           terminal.write(y, start_x, lines[i]);
682       }
683   },
684   full_refresh: function() {
685     terminal.drawBox(0, 0, terminal.rows, terminal.cols);
686     if (this.mode.is_intro) {
687         this.draw_history();
688         this.draw_input();
689     } else {
690         if (game.turn_complete) {
691             this.draw_map();
692             this.draw_turn_line();
693         }
694         this.draw_mode_line();
695         if (this.mode.shows_info) {
696           this.draw_info();
697         } else {
698           this.draw_history();
699         }
700         this.draw_input();
701     }
702     if (this.show_help) {
703         this.draw_help();
704     }
705     terminal.refresh();
706   }
707 }
708
709 let game = {
710     init: function() {
711         this.things = {};
712         this.turn = -1;
713         this.map = "";
714         this.map_control = "";
715         this.map_size = [0,0];
716         this.player_id = -1;
717         this.portals = {};
718         this.tasks = {};
719     },
720     get_thing: function(id_, create_if_not_found=false) {
721         if (id_ in game.things) {
722             return game.things[id_];
723         } else if (create_if_not_found) {
724             let t = new Thing([0,0]);
725             game.things[id_] = t;
726             return t;
727         };
728     },
729     move: function(start_position, direction) {
730         let target = [start_position[0], start_position[1]];
731         if (direction == 'LEFT') {
732             target[1] -= 1;
733         } else if (direction == 'RIGHT') {
734             target[1] += 1;
735         } else if (game.map_geometry == 'Square') {
736             if (direction == 'UP') {
737                 target[0] -= 1;
738             } else if (direction == 'DOWN') {
739                 target[0] += 1;
740             };
741         } else if (game.map_geometry == 'Hex') {
742             let start_indented = start_position[0] % 2;
743             if (direction == 'UPLEFT') {
744                 target[0] -= 1;
745                 if (!start_indented) {
746                     target[1] -= 1;
747                 }
748             } else if (direction == 'UPRIGHT') {
749                 target[0] -= 1;
750                 if (start_indented) {
751                     target[1] += 1;
752                 }
753             } else if (direction == 'DOWNLEFT') {
754                 target[0] += 1;
755                 if (!start_indented) {
756                     target[1] -= 1;
757                 }
758             } else if (direction == 'DOWNRIGHT') {
759                 target[0] += 1;
760                 if (start_indented) {
761                     target[1] += 1;
762                 }
763             };
764         };
765         if (target[0] < 0 || target[1] < 0 ||
766             target[0] >= this.map_size[0] || target[1] >= this.map_size[1]) {
767             return null;
768         };
769         return target;
770     },
771     teleport: function() {
772         let player = this.get_thing(game.player_id);
773         if (player.position in this.portals) {
774             server.reconnect_to(this.portals[player.position]);
775         } else {
776             terminal.blink_screen();
777             tui.log_msg('? not standing on portal')
778         }
779     }
780 }
781
782 game.init();
783 tui.init();
784 tui.full_refresh();
785 server.init(websocket_location);
786
787 let explorer = {
788     position: [0,0],
789     info_db: {},
790     info_hints: [],
791     move: function(direction) {
792         let target = game.move(this.position, direction);
793         if (target) {
794             this.position = target
795             this.query_info();
796         } else {
797             terminal.blink_screen();
798         };
799     },
800     update_info_db: function(yx, str) {
801         this.info_db[yx] = str;
802         if (tui.mode == mode_study) {
803             tui.full_refresh();
804         }
805     },
806     empty_info_db: function() {
807         this.info_db = {};
808         this.info_hints = [];
809         if (tui.mode == mode_study) {
810             tui.full_refresh();
811         }
812     },
813     query_info: function() {
814         server.send(["GET_ANNOTATION", unparser.to_yx(explorer.position)]);
815     },
816     get_info: function() {
817         let position_i = this.position[0] * game.map_size[1] + this.position[1];
818         if (game.fov[position_i] != '.') {
819             return 'outside field of view';
820         };
821         let info = "";
822         let terrain_char = game.map[position_i]
823         let terrain_desc = '?'
824         if (game.terrains[terrain_char]) {
825             terrain_desc = game.terrains[terrain_char];
826         };
827         info += 'TERRAIN: "' + terrain_char + '" / ' + terrain_desc + "\n";
828         let protection = game.map_control[position_i];
829         if (protection == '.') {
830             protection = 'unprotected';
831         };
832         info += 'PROTECTION: ' + protection + '\n';
833         for (let t_id in game.things) {
834              let t = game.things[t_id];
835              if (t.position[0] == this.position[0] && t.position[1] == this.position[1]) {
836                  let symbol = game.thing_types[t.type_];
837                  info += "THING: " + t.type_ + " / " + symbol;
838                  if (t.player_char) {
839                      info += t.player_char;
840                  };
841                  if (t.name_) {
842                      info += " (" + t.name_ + ")";
843                  }
844                  info += "\n";
845              }
846         }
847         if (this.position in game.portals) {
848             info += "PORTAL: " + game.portals[this.position] + "\n";
849         }
850         if (this.position in this.info_db) {
851             info += "ANNOTATIONS: " + this.info_db[this.position];
852         } else {
853             info += 'waiting …';
854         }
855         return info;
856     },
857     annotate: function(msg) {
858         if (msg.length == 0) {
859             msg = " ";  // triggers annotation deletion
860         }
861         server.send(["ANNOTATE", unparser.to_yx(explorer.position), msg, tui.password]);
862     },
863     set_portal: function(msg) {
864         if (msg.length == 0) {
865             msg = " ";  // triggers portal deletion
866         }
867         server.send(["PORTAL", unparser.to_yx(explorer.position), msg, tui.password]);
868     }
869 }
870
871 tui.inputEl.addEventListener('input', (event) => {
872     if (tui.mode.has_input_prompt) {
873         let max_length = tui.window_width * terminal.rows - tui.input_prompt.length;
874         if (tui.inputEl.value.length > max_length) {
875             tui.inputEl.value = tui.inputEl.value.slice(0, max_length);
876         };
877         tui.recalc_input_lines();
878     } else if (tui.mode == mode_edit && tui.inputEl.value.length > 0) {
879         server.send(["TASK:WRITE", tui.inputEl.value[0], tui.password]);
880         tui.switch_mode(mode_play);
881     }
882     tui.full_refresh();
883 }, false);
884 tui.inputEl.addEventListener('keydown', (event) => {
885     tui.show_help = false;
886     if (event.key == 'Enter') {
887         event.preventDefault();
888     }
889     if (tui.mode.has_input_prompt && event.key == 'Enter' && tui.inputEl.value == '/help') {
890         tui.show_help = true;
891         tui.empty_input();
892         tui.restore_input_values();
893     } else if (!tui.mode.has_input_prompt && event.key == tui.keys.help) {
894         tui.show_help = true;
895     } else if (tui.mode == mode_login && event.key == 'Enter') {
896         tui.login_name = tui.inputEl.value;
897         server.send(['LOGIN', tui.inputEl.value]);
898         tui.empty_input();
899     } else if (tui.mode == mode_portal && event.key == 'Enter') {
900         explorer.set_portal(tui.inputEl.value);
901         tui.switch_mode(mode_play);
902     } else if (tui.mode == mode_annotate && event.key == 'Enter') {
903         explorer.annotate(tui.inputEl.value);
904         tui.switch_mode(mode_play);
905     } else if (tui.mode == mode_password && event.key == 'Enter') {
906         if (tui.inputEl.value.length == 0) {
907             tui.inputEl.value = " ";
908         }
909         tui.password = tui.inputEl.value
910         tui.switch_mode(mode_play);
911     } else if (tui.mode == mode_chat && event.key == 'Enter') {
912         let tokens = parser.tokenize(tui.inputEl.value);
913         if (tokens.length > 0 && tokens[0].length > 0) {
914             if (tui.inputEl.value[0][0] == '/') {
915                 if (tokens[0].slice(1) == 'play' || tokens[0][1] == tui.keys.switch_to_play) {
916                     tui.switch_mode(mode_play);
917                 } else if (tokens[0].slice(1) == 'study' || tokens[0][1] == tui.keys.switch_to_study) {
918                     tui.switch_mode(mode_study);
919                 } else if (tokens[0].slice(1) == 'nick') {
920                     if (tokens.length > 1) {
921                         server.send(['NICK', tokens[1]]);
922                     } else {
923                         tui.log_msg('? need new name');
924                     }
925                 } else {
926                     tui.log_msg('? unknown command');
927                 }
928             } else {
929                     server.send(['ALL', tui.inputEl.value]);
930             }
931         } else if (tui.inputEl.valuelength > 0) {
932                 server.send(['ALL', tui.inputEl.value]);
933         }
934         tui.empty_input();
935     } else if (tui.mode == mode_play) {
936           if (event.key === tui.keys.switch_to_chat) {
937               event.preventDefault();
938               tui.switch_mode(mode_chat);
939           } else if (event.key === tui.keys.switch_to_edit
940                      && game.tasks.includes('WRITE')) {
941               event.preventDefault();
942               tui.switch_mode(mode_edit);
943           } else if (event.key === tui.keys.switch_to_study) {
944               tui.switch_mode(mode_study);
945           } else if (event.key === tui.keys.switch_to_password) {
946               event.preventDefault();
947               tui.switch_mode(mode_password);
948           } else if (event.key === tui.keys.flatten
949                      && game.tasks.includes('FLATTEN_SURROUNDINGS')) {
950               server.send(["TASK:FLATTEN_SURROUNDINGS", tui.password]);
951           } else if (event.key === tui.keys.take_thing
952                      && game.tasks.includes('PICK_UP')) {
953               server.send(["TASK:PICK_UP"]);
954           } else if (event.key === tui.keys.drop_thing
955                      && game.tasks.includes('DROP')) {
956               server.send(["TASK:DROP"]);
957           } else if (event.key in tui.movement_keys
958                      && game.tasks.includes('MOVE')) {
959               server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
960           } else if (event.key === tui.keys.teleport) {
961               game.teleport();
962           } else if (event.key === tui.keys.switch_to_portal) {
963               event.preventDefault();
964               tui.switch_mode(mode_portal);
965           } else if (event.key === tui.keys.switch_to_annotate) {
966               event.preventDefault();
967               tui.switch_mode(mode_annotate);
968           };
969     } else if (tui.mode == mode_study) {
970         if (event.key === tui.keys.switch_to_chat) {
971             event.preventDefault();
972             tui.switch_mode(mode_chat);
973         } else if (event.key == tui.keys.switch_to_play) {
974             tui.switch_mode(mode_play);
975         } else if (event.key in tui.movement_keys) {
976             explorer.move(tui.movement_keys[event.key]);
977         } else if (event.key == tui.keys.toggle_map_mode) {
978             if (tui.map_mode == 'terrain') {
979                 tui.map_mode = 'annotations';
980             } else if (tui.map_mode == 'annotations') {
981                 tui.map_mode = 'control';
982             } else {
983                 tui.map_mode = 'terrain';
984             }
985         };
986     }
987     tui.full_refresh();
988 }, false);
989
990 rows_selector.addEventListener('input', function() {
991     if (rows_selector.value % 4 != 0 || rows_selector.value < 24) {
992         return;
993     }
994     window.localStorage.setItem(rows_selector.id, rows_selector.value);
995     terminal.initialize();
996     tui.full_refresh();
997 }, false);
998 cols_selector.addEventListener('input', function() {
999     if (cols_selector.value % 4 != 0 || cols_selector.value < 80) {
1000         return;
1001     }
1002     window.localStorage.setItem(cols_selector.id, cols_selector.value);
1003     terminal.initialize();
1004     tui.window_width = terminal.cols / 2,
1005     tui.full_refresh();
1006 }, false);
1007 for (let key_selector of key_selectors) {
1008     key_selector.addEventListener('input', function() {
1009         window.localStorage.setItem(key_selector.id, key_selector.value);
1010         tui.init_keys();
1011     }, false);
1012 }
1013 window.setInterval(function() {
1014     if (server.connected) {
1015         server.send(['PING']);
1016     } else {
1017         server.reconnect_to(server.url);
1018         tui.log_msg('@ attempting reconnect …')
1019     }
1020 }, 5000);
1021 document.getElementById("terminal").onclick = function() {
1022     tui.inputEl.focus();
1023 };
1024 document.getElementById("help").onclick = function() {
1025     tui.show_help = true;
1026     tui.full_refresh();
1027 };
1028 document.getElementById("switch_to_play").onclick = function() {
1029     tui.switch_mode(mode_play);
1030     tui.full_refresh();
1031 };
1032 document.getElementById("switch_to_study").onclick = function() {
1033     tui.switch_mode(mode_study);
1034     tui.full_refresh();
1035 };
1036 document.getElementById("switch_to_chat").onclick = function() {
1037     tui.switch_mode(mode_chat);
1038     tui.full_refresh();
1039 };
1040 document.getElementById("switch_to_password").onclick = function() {
1041     tui.switch_mode(mode_password);
1042     tui.full_refresh();
1043 };
1044 document.getElementById("switch_to_edit").onclick = function() {
1045     tui.switch_mode(mode_edit);
1046     tui.full_refresh();
1047 };
1048 document.getElementById("switch_to_annotate").onclick = function() {
1049     tui.switch_mode(mode_annotate);
1050     tui.full_refresh();
1051 };
1052 document.getElementById("switch_to_portal").onclick = function() {
1053     tui.switch_mode(mode_portal);
1054     tui.full_refresh();
1055 };
1056 document.getElementById("toggle_map_mode").onclick = function() {
1057     if (tui.map_mode == 'terrain') {
1058         tui.map_mode = 'annotations';
1059     } else if (tui.map_mode == 'annotations') {
1060         tui.map_mode = 'control';
1061     } else {
1062         tui.map_mode = 'terrain';
1063     }
1064     tui.full_refresh();
1065 };
1066 document.getElementById("take_thing").onclick = function() {
1067         server.send(['TASK:PICK_UP']);
1068 };
1069 document.getElementById("drop_thing").onclick = function() {
1070         server.send(['TASK:DROP']);
1071 };
1072 document.getElementById("flatten").onclick = function() {
1073     server.send(['TASK:FLATTEN_SURROUNDINGS', tui.password]);
1074 };
1075 document.getElementById("teleport").onclick = function() {
1076     game.teleport();
1077 };
1078 document.getElementById("move_upleft").onclick = function() {
1079     if (tui.mode == mode_play) {
1080         server.send(['TASK:MOVE', 'UPLEFT']);
1081     } else {
1082         explorer.move('UPLEFT');
1083     };
1084 };
1085 document.getElementById("move_left").onclick = function() {
1086     if (tui.mode == mode_play) {
1087         server.send(['TASK:MOVE', 'LEFT']);
1088     } else {
1089         explorer.move('LEFT');
1090     };
1091 };
1092 document.getElementById("move_downleft").onclick = function() {
1093     if (tui.mode == mode_play) {
1094         server.send(['TASK:MOVE', 'DOWNLEFT']);
1095     } else {
1096         explorer.move('DOWNLEFT');
1097     };
1098 };
1099 document.getElementById("move_down").onclick = function() {
1100     if (tui.mode == mode_play) {
1101         server.send(['TASK:MOVE', 'DOWN']);
1102     } else {
1103         explorer.move('DOWN');
1104     };
1105 };
1106 document.getElementById("move_up").onclick = function() {
1107     if (tui.mode == mode_play) {
1108         server.send(['TASK:MOVE', 'UP']);
1109     } else {
1110         explorer.move('UP');
1111     };
1112 };
1113 document.getElementById("move_upright").onclick = function() {
1114     if (tui.mode == mode_play) {
1115         server.send(['TASK:MOVE', 'UPRIGHT']);
1116     } else {
1117         explorer.move('UPRIGHT');
1118     };
1119 };
1120 document.getElementById("move_right").onclick = function() {
1121     if (tui.mode == mode_play) {
1122         server.send(['TASK:MOVE', 'RIGHT']);
1123     } else {
1124         explorer.move('RIGHT');
1125     };
1126 };
1127 document.getElementById("move_downright").onclick = function() {
1128     if (tui.mode == mode_play) {
1129         server.send(['TASK:MOVE', 'DOWNRIGHT']);
1130     } else {
1131         explorer.move('DOWNRIGHT');
1132     };
1133 };
1134 </script>
1135 </body></html>