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