home · contact · privacy
dd annotation hints view.
[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         for (let t_id in game.things) {
829              let t = game.things[t_id];
830              if (t.position[0] == this.position[0] && t.position[1] == this.position[1]) {
831                  let symbol = game.thing_types[t.type_];
832                  info += "THING: " + t.type_ + " / " + symbol;
833                  if (t.player_char) {
834                      info += t.player_char;
835                  };
836                  if (t.name_) {
837                      info += " (" + t.name_ + ")";
838                  }
839                  info += "\n";
840              }
841         }
842         if (this.position in game.portals) {
843             info += "PORTAL: " + game.portals[this.position] + "\n";
844         }
845         if (this.position in this.info_db) {
846             info += "ANNOTATIONS: " + this.info_db[this.position];
847         } else {
848             info += 'waiting …';
849         }
850         return info;
851     },
852     annotate: function(msg) {
853         if (msg.length == 0) {
854             msg = " ";  // triggers annotation deletion
855         }
856         server.send(["ANNOTATE", unparser.to_yx(explorer.position), msg, tui.password]);
857     },
858     set_portal: function(msg) {
859         if (msg.length == 0) {
860             msg = " ";  // triggers portal deletion
861         }
862         server.send(["PORTAL", unparser.to_yx(explorer.position), msg, tui.password]);
863     }
864 }
865
866 tui.inputEl.addEventListener('input', (event) => {
867     if (tui.mode.has_input_prompt) {
868         let max_length = tui.window_width * terminal.rows - tui.input_prompt.length;
869         if (tui.inputEl.value.length > max_length) {
870             tui.inputEl.value = tui.inputEl.value.slice(0, max_length);
871         };
872         tui.recalc_input_lines();
873     } else if (tui.mode == mode_edit && tui.inputEl.value.length > 0) {
874         server.send(["TASK:WRITE", tui.inputEl.value[0], tui.password]);
875         tui.switch_mode(mode_play);
876     }
877     tui.full_refresh();
878 }, false);
879 tui.inputEl.addEventListener('keydown', (event) => {
880     tui.show_help = false;
881     if (event.key == 'Enter') {
882         event.preventDefault();
883     }
884     if (tui.mode.has_input_prompt && event.key == 'Enter' && tui.inputEl.value == '/help') {
885         tui.show_help = true;
886         tui.empty_input();
887         tui.restore_input_values();
888     } else if (!tui.mode.has_input_prompt && event.key == tui.keys.help) {
889         tui.show_help = true;
890     } else if (tui.mode == mode_login && event.key == 'Enter') {
891         tui.login_name = tui.inputEl.value;
892         server.send(['LOGIN', tui.inputEl.value]);
893         tui.empty_input();
894     } else if (tui.mode == mode_portal && event.key == 'Enter') {
895         explorer.set_portal(tui.inputEl.value);
896         tui.switch_mode(mode_play);
897     } else if (tui.mode == mode_annotate && event.key == 'Enter') {
898         explorer.annotate(tui.inputEl.value);
899         tui.switch_mode(mode_play);
900     } else if (tui.mode == mode_password && event.key == 'Enter') {
901         if (tui.inputEl.value.length == 0) {
902             tui.inputEl.value = " ";
903         }
904         tui.password = tui.inputEl.value
905         tui.switch_mode(mode_play);
906     } else if (tui.mode == mode_chat && event.key == 'Enter') {
907         let tokens = parser.tokenize(tui.inputEl.value);
908         if (tokens.length > 0 && tokens[0].length > 0) {
909             if (tui.inputEl.value[0][0] == '/') {
910                 if (tokens[0].slice(1) == 'play' || tokens[0][1] == tui.keys.switch_to_play) {
911                     tui.switch_mode(mode_play);
912                 } else if (tokens[0].slice(1) == 'study' || tokens[0][1] == tui.keys.switch_to_study) {
913                     tui.switch_mode(mode_study);
914                 } else if (tokens[0].slice(1) == 'nick') {
915                     if (tokens.length > 1) {
916                         server.send(['NICK', tokens[1]]);
917                     } else {
918                         tui.log_msg('? need new name');
919                     }
920                 } else {
921                     tui.log_msg('? unknown command');
922                 }
923             } else {
924                     server.send(['ALL', tui.inputEl.value]);
925             }
926         } else if (tui.inputEl.valuelength > 0) {
927                 server.send(['ALL', tui.inputEl.value]);
928         }
929         tui.empty_input();
930     } else if (tui.mode == mode_play) {
931           if (event.key === tui.keys.switch_to_chat) {
932               event.preventDefault();
933               tui.switch_mode(mode_chat);
934           } else if (event.key === tui.keys.switch_to_edit
935                      && game.tasks.includes('WRITE')) {
936               event.preventDefault();
937               tui.switch_mode(mode_edit);
938           } else if (event.key === tui.keys.switch_to_study) {
939               tui.switch_mode(mode_study);
940           } else if (event.key === tui.keys.switch_to_password) {
941               event.preventDefault();
942               tui.switch_mode(mode_password);
943           } else if (event.key === tui.keys.flatten
944                      && game.tasks.includes('FLATTEN_SURROUNDINGS')) {
945               server.send(["TASK:FLATTEN_SURROUNDINGS", tui.password]);
946           } else if (event.key === tui.keys.take_thing
947                      && game.tasks.includes('PICK_UP')) {
948               server.send(["TASK:PICK_UP"]);
949           } else if (event.key === tui.keys.drop_thing
950                      && game.tasks.includes('DROP')) {
951               server.send(["TASK:DROP"]);
952           } else if (event.key in tui.movement_keys
953                      && game.tasks.includes('MOVE')) {
954               server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
955           } else if (event.key === tui.keys.teleport) {
956               game.teleport();
957           } else if (event.key === tui.keys.switch_to_portal) {
958               event.preventDefault();
959               tui.switch_mode(mode_portal);
960           } else if (event.key === tui.keys.switch_to_annotate) {
961               event.preventDefault();
962               tui.switch_mode(mode_annotate);
963           };
964     } else if (tui.mode == mode_study) {
965         if (event.key === tui.keys.switch_to_chat) {
966             event.preventDefault();
967             tui.switch_mode(mode_chat);
968         } else if (event.key == tui.keys.switch_to_play) {
969             tui.switch_mode(mode_play);
970         } else if (event.key in tui.movement_keys) {
971             explorer.move(tui.movement_keys[event.key]);
972         } else if (event.key == tui.keys.toggle_map_mode) {
973             if (tui.map_mode == 'terrain') {
974                 tui.map_mode = 'annotations';
975             } else if (tui.map_mode == 'annotations') {
976                 tui.map_mode = 'control';
977             } else {
978                 tui.map_mode = 'terrain';
979             }
980         };
981     }
982     tui.full_refresh();
983 }, false);
984
985 rows_selector.addEventListener('input', function() {
986     if (rows_selector.value % 4 != 0 || rows_selector.value < 24) {
987         return;
988     }
989     window.localStorage.setItem(rows_selector.id, rows_selector.value);
990     terminal.initialize();
991     tui.full_refresh();
992 }, false);
993 cols_selector.addEventListener('input', function() {
994     if (cols_selector.value % 4 != 0 || cols_selector.value < 80) {
995         return;
996     }
997     window.localStorage.setItem(cols_selector.id, cols_selector.value);
998     terminal.initialize();
999     tui.window_width = terminal.cols / 2,
1000     tui.full_refresh();
1001 }, false);
1002 for (let key_selector of key_selectors) {
1003     key_selector.addEventListener('input', function() {
1004         window.localStorage.setItem(key_selector.id, key_selector.value);
1005         tui.init_keys();
1006     }, false);
1007 }
1008 window.setInterval(function() {
1009     if (server.connected) {
1010         server.send(['PING']);
1011     } else {
1012         server.reconnect_to(server.url);
1013         tui.log_msg('@ attempting reconnect …')
1014     }
1015 }, 5000);
1016 document.getElementById("terminal").onclick = function() {
1017     tui.inputEl.focus();
1018 };
1019 document.getElementById("help").onclick = function() {
1020     tui.show_help = true;
1021     tui.full_refresh();
1022 };
1023 document.getElementById("switch_to_play").onclick = function() {
1024     tui.switch_mode(mode_play);
1025     tui.full_refresh();
1026 };
1027 document.getElementById("switch_to_study").onclick = function() {
1028     tui.switch_mode(mode_study);
1029     tui.full_refresh();
1030 };
1031 document.getElementById("switch_to_chat").onclick = function() {
1032     tui.switch_mode(mode_chat);
1033     tui.full_refresh();
1034 };
1035 document.getElementById("switch_to_password").onclick = function() {
1036     tui.switch_mode(mode_password);
1037     tui.full_refresh();
1038 };
1039 document.getElementById("switch_to_edit").onclick = function() {
1040     tui.switch_mode(mode_edit);
1041     tui.full_refresh();
1042 };
1043 document.getElementById("switch_to_annotate").onclick = function() {
1044     tui.switch_mode(mode_annotate);
1045     tui.full_refresh();
1046 };
1047 document.getElementById("switch_to_portal").onclick = function() {
1048     tui.switch_mode(mode_portal);
1049     tui.full_refresh();
1050 };
1051 document.getElementById("toggle_map_mode").onclick = function() {
1052     if (tui.map_mode == 'terrain') {
1053         tui.map_mode = 'annotations';
1054     } else if (tui.map_mode == 'annotations') {
1055         tui.map_mode = 'control';
1056     } else {
1057         tui.map_mode = 'terrain';
1058     }
1059     tui.full_refresh();
1060 };
1061 document.getElementById("take_thing").onclick = function() {
1062         server.send(['TASK:PICK_UP']);
1063 };
1064 document.getElementById("drop_thing").onclick = function() {
1065         server.send(['TASK:DROP']);
1066 };
1067 document.getElementById("flatten").onclick = function() {
1068     server.send(['TASK:FLATTEN_SURROUNDINGS', tui.password]);
1069 };
1070 document.getElementById("teleport").onclick = function() {
1071     game.teleport();
1072 };
1073 document.getElementById("move_upleft").onclick = function() {
1074     if (tui.mode == mode_play) {
1075         server.send(['TASK:MOVE', 'UPLEFT']);
1076     } else {
1077         explorer.move('UPLEFT');
1078     };
1079 };
1080 document.getElementById("move_left").onclick = function() {
1081     if (tui.mode == mode_play) {
1082         server.send(['TASK:MOVE', 'LEFT']);
1083     } else {
1084         explorer.move('LEFT');
1085     };
1086 };
1087 document.getElementById("move_downleft").onclick = function() {
1088     if (tui.mode == mode_play) {
1089         server.send(['TASK:MOVE', 'DOWNLEFT']);
1090     } else {
1091         explorer.move('DOWNLEFT');
1092     };
1093 };
1094 document.getElementById("move_down").onclick = function() {
1095     if (tui.mode == mode_play) {
1096         server.send(['TASK:MOVE', 'DOWN']);
1097     } else {
1098         explorer.move('DOWN');
1099     };
1100 };
1101 document.getElementById("move_up").onclick = function() {
1102     if (tui.mode == mode_play) {
1103         server.send(['TASK:MOVE', 'UP']);
1104     } else {
1105         explorer.move('UP');
1106     };
1107 };
1108 document.getElementById("move_upright").onclick = function() {
1109     if (tui.mode == mode_play) {
1110         server.send(['TASK:MOVE', 'UPRIGHT']);
1111     } else {
1112         explorer.move('UPRIGHT');
1113     };
1114 };
1115 document.getElementById("move_right").onclick = function() {
1116     if (tui.mode == mode_play) {
1117         server.send(['TASK:MOVE', 'RIGHT']);
1118     } else {
1119         explorer.move('RIGHT');
1120     };
1121 };
1122 document.getElementById("move_downright").onclick = function() {
1123     if (tui.mode == mode_play) {
1124         server.send(['TASK:MOVE', 'DOWNRIGHT']);
1125     } else {
1126         explorer.move('DOWNRIGHT');
1127     };
1128 };
1129 </script>
1130 </body></html>