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