home · contact · privacy
0890647eb7d288081664946aea3e6394f02a7d49
[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.inputEl.focus();
383     this.show_help = false;
384     this.map_mode = 'terrain';
385     if (mode.shows_info && game.player_id in game.things) {
386       explorer.position = game.things[game.player_id].position;
387     }
388     this.mode = mode;
389     this.empty_input();
390     this.restore_input_values();
391     document.getElementById("take_thing").disabled = true;
392     document.getElementById("drop_thing").disabled = true;
393     document.getElementById("flatten").disabled = true;
394     document.getElementById("teleport").disabled = true;
395     document.getElementById("toggle_map_mode").disabled = true;
396     document.getElementById("switch_to_chat").disabled = true;
397     document.getElementById("switch_to_play").disabled = true;
398     document.getElementById("switch_to_study").disabled = true;
399     document.getElementById("switch_to_edit").disabled = true;
400     document.getElementById("switch_to_portal").disabled = true;
401     document.getElementById("switch_to_annotate").disabled = true;
402     document.getElementById("switch_to_password").disabled = true;
403     document.getElementById("move_left").disabled = true;
404     document.getElementById("move_upleft").disabled = true;
405     document.getElementById("move_up").disabled = true;
406     document.getElementById("move_upright").disabled = true;
407     document.getElementById("move_downleft").disabled = true;
408     document.getElementById("move_down").disabled = true;
409     document.getElementById("move_downright").disabled = true;
410     document.getElementById("move_right").disabled = true;
411     if (mode == mode_play || mode == mode_study) {
412         document.getElementById("move_left").disabled = false;
413         document.getElementById("move_right").disabled = false;
414         if (game.map_geometry == 'Hex') {
415             document.getElementById("move_upleft").disabled = false;
416             document.getElementById("move_upright").disabled = false;
417             document.getElementById("move_downleft").disabled = false;
418             document.getElementById("move_downright").disabled = false;
419         } else {
420             document.getElementById("move_up").disabled = false;
421             document.getElementById("move_down").disabled = false;
422         }
423     }
424     if (!mode.is_intro && mode != mode_play) {
425         document.getElementById("switch_to_play").disabled = false;
426     }
427     if (!mode.is_intro && mode != mode_study) {
428         document.getElementById("switch_to_study").disabled = false;
429     }
430     if (!mode.is_intro && mode != mode_chat) {
431         document.getElementById("switch_to_chat").disabled = false;
432     }
433     if (mode == mode_login) {
434         if (this.login_name) {
435             server.send(['LOGIN', this.login_name]);
436         } else {
437             this.log_msg("? need login name");
438         }
439     } else if (mode == mode_play) {
440         if (game.tasks.includes('PICK_UP')) {
441             document.getElementById("take_thing").disabled = false;
442         }
443         if (game.tasks.includes('DROP')) {
444             document.getElementById("drop_thing").disabled = false;
445         }
446         if (game.tasks.includes('FLATTEN_SURROUNDINGS')) {
447             document.getElementById("flatten").disabled = false;
448         }
449         if (game.tasks.includes('MOVE')) {
450         }
451         document.getElementById("teleport").disabled = false;
452         document.getElementById("switch_to_annotate").disabled = false;
453         document.getElementById("switch_to_edit").disabled = false;
454         document.getElementById("switch_to_portal").disabled = false;
455         document.getElementById("switch_to_password").disabled = false;
456     } else if (mode == mode_study) {
457         document.getElementById("toggle_map_mode").disabled = false;
458     } else if (mode == mode_edit) {
459         this.show_help = true;
460     }
461     this.full_refresh();
462   },
463   restore_input_values: function() {
464       if (this.mode == mode_annotate && explorer.position in explorer.info_db) {
465           let info = explorer.info_db[explorer.position];
466           if (info != "(none)") {
467               this.inputEl.value = info;
468               this.recalc_input_lines();
469           }
470       } else if (this.mode == mode_portal && explorer.position in game.portals) {
471           let portal = game.portals[explorer.position]
472           this.inputEl.value = portal;
473           this.recalc_input_lines();
474       } else if (this.mode == mode_password) {
475           this.inputEl.value = this.password;
476           this.recalc_input_lines();
477       }
478   },
479   empty_input: function(str) {
480       this.inputEl.value = "";
481       if (this.mode.has_input_prompt) {
482           this.recalc_input_lines();
483       } else {
484           this.height_input = 0;
485       }
486   },
487   recalc_input_lines: function() {
488       this.input_lines = this.msg_into_lines_of_width(this.input_prompt + this.inputEl.value, this.window_width);
489       this.height_input = this.input_lines.length;
490   },
491   msg_into_lines_of_width: function(msg, width) {
492     let chunk = "";
493     let lines = [];
494     for (let i = 0, x = 0; i < msg.length; i++, x++) {
495       if (x >= width || msg[i] == "\n") {
496         lines.push(chunk);
497         chunk = "";
498         x = 0;
499       };
500       if (msg[i] != "\n") {
501         chunk += msg[i];
502       }
503     }
504     lines.push(chunk);
505     return lines;
506   },
507   log_msg: function(msg) {
508       this.log.push(msg);
509       while (this.log.length > 100) {
510         this.log.shift();
511       };
512       this.full_refresh();
513   },
514   draw_map: function() {
515     let map_lines_split = [];
516     let line = [];
517     let map_content = game.map;
518     if (this.map_mode == 'control') {
519         map_content = game.map_control;
520     }
521     for (let i = 0, j = 0; i < game.map.length; i++, j++) {
522         if (j == game.map_size[1]) {
523             map_lines_split.push(line);
524             line = [];
525             j = 0;
526         };
527         line.push(map_content[i] + ' ');
528     };
529     map_lines_split.push(line);
530     if (this.map_mode == 'terrain') {
531         for (const p in game.portals) {
532             let coordinate = p.split(',')
533             map_lines_split[coordinate[0]][coordinate[1]] = 'P ';
534         }
535         let used_positions = [];
536         for (const thing_id in game.things) {
537             let t = game.things[thing_id];
538             let symbol = game.thing_types[t.type_];
539             let meta_char = ' ';
540             if (t.player_char) {
541                 meta_char = t.player_char;
542             }
543             if (used_positions.includes(t.position.toString())) {
544                 meta_char = '+';
545             };
546             map_lines_split[t.position[0]][t.position[1]] = symbol + meta_char;
547             used_positions.push(t.position.toString());
548         };
549     }
550     if (tui.mode.shows_info) {
551         map_lines_split[explorer.position[0]][explorer.position[1]] = '??';
552     }
553     let map_lines = []
554     if (game.map_geometry == 'Square') {
555         for (let line_split of map_lines_split) {
556             map_lines.push(line_split.join(''));
557         };
558     } else if (game.map_geometry == 'Hex') {
559         let indent = 0
560         for (let line_split of map_lines_split) {
561             map_lines.push(' '.repeat(indent) + line_split.join(''));
562             if (indent == 0) {
563                 indent = 1;
564             } else {
565                 indent = 0;
566             };
567         };
568     }
569     let window_center = [terminal.rows / 2, this.window_width / 2];
570     let player = game.things[game.player_id];
571     let center_position = [player.position[0], player.position[1]];
572     if (tui.mode.shows_info) {
573         center_position = [explorer.position[0], explorer.position[1]];
574     }
575     center_position[1] = center_position[1] * 2;
576     let offset = [center_position[0] - window_center[0],
577                   center_position[1] - window_center[1]]
578     if (game.map_geometry == 'Hex' && offset[0] % 2) {
579         offset[1] += 1;
580     };
581     let term_y = Math.max(0, -offset[0]);
582     let term_x = Math.max(0, -offset[1]);
583     let map_y = Math.max(0, offset[0]);
584     let map_x = Math.max(0, offset[1]);
585     for (; term_y < terminal.rows && map_y < game.map_size[0]; term_y++, map_y++) {
586         let to_draw = map_lines[map_y].slice(map_x, this.window_width + offset[1]);
587         terminal.write(term_y, term_x, to_draw);
588     }
589   },
590   draw_mode_line: function() {
591       let help = 'hit [' + this.keys.help + '] for help';
592       if (this.mode.has_input_prompt) {
593           help = 'enter /help for help';
594       }
595       terminal.write(0, this.window_width, 'MODE: ' + this.mode.name + ' – ' + help);
596   },
597   draw_turn_line: function(n) {
598     terminal.write(1, this.window_width, 'TURN: ' + game.turn);
599   },
600   draw_history: function() {
601       let log_display_lines = [];
602       for (let line of this.log) {
603           log_display_lines = log_display_lines.concat(this.msg_into_lines_of_width(line, this.window_width));
604       };
605       for (let y = terminal.rows - 1 - this.height_input,
606                i = log_display_lines.length - 1;
607            y >= this.height_header && i >= 0;
608            y--, i--) {
609           terminal.write(y, this.window_width, log_display_lines[i]);
610       }
611   },
612   draw_info: function() {
613     let lines = this.msg_into_lines_of_width(explorer.get_info(), this.window_width);
614     for (let y = this.height_header, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
615       terminal.write(y, this.window_width, lines[i]);
616     }
617   },
618   draw_input: function() {
619     if (this.mode.has_input_prompt) {
620         for (let y = terminal.rows - this.height_input, i = 0; i < this.input_lines.length; y++, i++) {
621             terminal.write(y, this.window_width, this.input_lines[i]);
622         }
623     }
624   },
625   draw_help: function() {
626       let movement_keys_desc = Object.keys(this.movement_keys).join(',');
627       let content = this.mode.name + " mode help\n\n" + this.mode.help_intro + "\n\n";
628       if (this.mode == mode_play) {
629           content += "Available actions:\n";
630           if (game.tasks.includes('MOVE')) {
631               content += "[" + movement_keys_desc + "] – move player\n";
632           }
633           if (game.tasks.includes('PICK_UP')) {
634               content += "[" + this.keys.take_thing + "] – take thing under player\n";
635           }
636           if (game.tasks.includes('DROP')) {
637               content += "[" + this.keys.drop_thing + "] – drop carried thing\n";
638           }
639           if (game.tasks.includes('FLATTEN_SURROUNDINGS')) {
640               content += "[" + tui.keys.flatten + "] – flatten player's surroundings\n";
641           }
642           content += "[" + tui.keys.teleport + "] – teleport to other space\n";
643           content += '\nOther modes available from here:\n';
644           content += '[' + this.keys.switch_to_chat + '] – chat mode\n';
645           content += '[' + this.keys.switch_to_study + '] – study mode\n';
646           content += '[' + this.keys.switch_to_edit + '] – terrain edit mode\n';
647           content += '[' + this.keys.switch_to_portal + '] – portal edit mode\n';
648           content += '[' + this.keys.switch_to_annotate + '] – annotation mode\n';
649           content += '[' + this.keys.switch_to_password + '] – password input mode\n';
650       } else if (this.mode == mode_study) {
651           content += "Available actions:\n";
652           content += '[' + movement_keys_desc + '] – move question mark\n';
653           content += '[' + this.keys.toggle_map_mode + '] – toggle view between terrain, and password protection areas\n';
654           content += '\nOther modes available from here:\n';
655           content += '[' + this.keys.switch_to_chat + '] – chat mode\n';
656           content += '[' + this.keys.switch_to_play + '] – play mode\n';
657       } else if (this.mode == mode_chat) {
658           content += '/nick NAME – re-name yourself to NAME\n';
659           content += '/' + this.keys.switch_to_play + ' or /play – switch to play mode\n';
660           content += '/' + this.keys.switch_to_study + ' or /study – switch to study mode\n';
661       }
662       let start_x = 0;
663       if (!this.mode.has_input_prompt) {
664           start_x = this.window_width
665       }
666       terminal.drawBox(0, start_x, terminal.rows, this.window_width);
667       let lines = this.msg_into_lines_of_width(content, this.window_width);
668       for (let y = 0, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
669           terminal.write(y, start_x, lines[i]);
670       }
671   },
672   full_refresh: function() {
673     terminal.drawBox(0, 0, terminal.rows, terminal.cols);
674     if (this.mode.is_intro) {
675         this.draw_history();
676         this.draw_input();
677     } else {
678         if (game.turn_complete) {
679             this.draw_map();
680             this.draw_turn_line();
681         }
682         this.draw_mode_line();
683         if (this.mode.shows_info) {
684           this.draw_info();
685         } else {
686           this.draw_history();
687         }
688         this.draw_input();
689     }
690     if (this.show_help) {
691         this.draw_help();
692     }
693     terminal.refresh();
694   }
695 }
696
697 let game = {
698     init: function() {
699         this.things = {};
700         this.turn = -1;
701         this.map = "";
702         this.map_control = "";
703         this.map_size = [0,0];
704         this.player_id = -1;
705         this.portals = {};
706         this.tasks = {};
707     },
708     get_thing: function(id_, create_if_not_found=false) {
709         if (id_ in game.things) {
710             return game.things[id_];
711         } else if (create_if_not_found) {
712             let t = new Thing([0,0]);
713             game.things[id_] = t;
714             return t;
715         };
716     },
717     move: function(start_position, direction) {
718         let target = [start_position[0], start_position[1]];
719         if (direction == 'LEFT') {
720             target[1] -= 1;
721         } else if (direction == 'RIGHT') {
722             target[1] += 1;
723         } else if (game.map_geometry == 'Square') {
724             if (direction == 'UP') {
725                 target[0] -= 1;
726             } else if (direction == 'DOWN') {
727                 target[0] += 1;
728             };
729         } else if (game.map_geometry == 'Hex') {
730             let start_indented = start_position[0] % 2;
731             if (direction == 'UPLEFT') {
732                 target[0] -= 1;
733                 if (!start_indented) {
734                     target[1] -= 1;
735                 }
736             } else if (direction == 'UPRIGHT') {
737                 target[0] -= 1;
738                 if (start_indented) {
739                     target[1] += 1;
740                 }
741             } else if (direction == 'DOWNLEFT') {
742                 target[0] += 1;
743                 if (!start_indented) {
744                     target[1] -= 1;
745                 }
746             } else if (direction == 'DOWNRIGHT') {
747                 target[0] += 1;
748                 if (start_indented) {
749                     target[1] += 1;
750                 }
751             };
752         };
753         if (target[0] < 0 || target[1] < 0 ||
754             target[0] >= this.map_size[0] || target[1] >= this.map_size[1]) {
755             return null;
756         };
757         return target;
758     },
759     teleport: function() {
760         let player = this.get_thing(game.player_id);
761         if (player.position in this.portals) {
762             server.reconnect_to(this.portals[player.position]);
763         } else {
764             terminal.blink_screen();
765             tui.log_msg('? not standing on portal')
766         }
767     }
768 }
769
770 game.init();
771 tui.init();
772 tui.full_refresh();
773 server.init(websocket_location);
774
775 let explorer = {
776     position: [0,0],
777     info_db: {},
778     move: function(direction) {
779         let target = game.move(this.position, direction);
780         if (target) {
781             this.position = target
782             this.query_info();
783         } else {
784             terminal.blink_screen();
785         };
786     },
787     update_info_db: function(yx, str) {
788         this.info_db[yx] = str;
789         if (tui.mode == mode_study) {
790             tui.full_refresh();
791         }
792     },
793     empty_info_db: function() {
794         this.info_db = {};
795         if (tui.mode == mode_study) {
796             tui.full_refresh();
797         }
798     },
799     query_info: function() {
800         server.send(["GET_ANNOTATION", unparser.to_yx(explorer.position)]);
801     },
802     get_info: function() {
803         let position_i = this.position[0] * game.map_size[1] + this.position[1];
804         if (game.fov[position_i] != '.') {
805             return 'outside field of view';
806         };
807         let info = "";
808         let terrain_char = game.map[position_i]
809         let terrain_desc = '?'
810         if (game.terrains[terrain_char]) {
811             terrain_desc = game.terrains[terrain_char];
812         };
813         info += 'TERRAIN: "' + terrain_char + '" / ' + terrain_desc + "\n";
814         for (let t_id in game.things) {
815              let t = game.things[t_id];
816              if (t.position[0] == this.position[0] && t.position[1] == this.position[1]) {
817                  let symbol = game.thing_types[t.type_];
818                  info += "THING: " + t.type_ + " / " + symbol;
819                  if (t.player_char) {
820                      info += t.player_char;
821                  };
822                  if (t.name_) {
823                      info += " (" + t.name_ + ")";
824                  }
825                  info += "\n";
826              }
827         }
828         if (this.position in game.portals) {
829             info += "PORTAL: " + game.portals[this.position] + "\n";
830         }
831         if (this.position in this.info_db) {
832             info += "ANNOTATIONS: " + this.info_db[this.position];
833         } else {
834             info += 'waiting …';
835         }
836         return info;
837     },
838     annotate: function(msg) {
839         if (msg.length == 0) {
840             msg = " ";  // triggers annotation deletion
841         }
842         server.send(["ANNOTATE", unparser.to_yx(explorer.position), msg, tui.password]);
843     },
844     set_portal: function(msg) {
845         if (msg.length == 0) {
846             msg = " ";  // triggers portal deletion
847         }
848         server.send(["PORTAL", unparser.to_yx(explorer.position), msg, tui.password]);
849     }
850 }
851
852 tui.inputEl.addEventListener('input', (event) => {
853     if (tui.mode.has_input_prompt) {
854         let max_length = tui.window_width * terminal.rows - tui.input_prompt.length;
855         if (tui.inputEl.value.length > max_length) {
856             tui.inputEl.value = tui.inputEl.value.slice(0, max_length);
857         };
858         tui.recalc_input_lines();
859     } else if (tui.mode == mode_edit && tui.inputEl.value.length > 0) {
860         server.send(["TASK:WRITE", tui.inputEl.value[0], tui.password]);
861         tui.switch_mode(mode_play);
862     }
863     tui.full_refresh();
864 }, false);
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 (server.connected) {
994         server.send(['PING']);
995     } else {
996         server.reconnect_to(server.url);
997         tui.log_msg('@ attempting reconnect …')
998     }
999 }, 5000);
1000 document.getElementById("terminal").onclick = function() {
1001     tui.inputEl.focus();
1002 };
1003 document.getElementById("help").onclick = function() {
1004     tui.show_help = true;
1005     tui.full_refresh();
1006 };
1007 document.getElementById("switch_to_play").onclick = function() {
1008     tui.switch_mode(mode_play);
1009     tui.full_refresh();
1010 };
1011 document.getElementById("switch_to_study").onclick = function() {
1012     tui.switch_mode(mode_study);
1013     tui.full_refresh();
1014 };
1015 document.getElementById("switch_to_chat").onclick = function() {
1016     tui.switch_mode(mode_chat);
1017     tui.full_refresh();
1018 };
1019 document.getElementById("switch_to_password").onclick = function() {
1020     tui.switch_mode(mode_password);
1021     tui.full_refresh();
1022 };
1023 document.getElementById("switch_to_edit").onclick = function() {
1024     tui.switch_mode(mode_edit);
1025     tui.full_refresh();
1026 };
1027 document.getElementById("switch_to_annotate").onclick = function() {
1028     tui.switch_mode(mode_annotate);
1029     tui.full_refresh();
1030 };
1031 document.getElementById("switch_to_portal").onclick = function() {
1032     tui.switch_mode(mode_portal);
1033     tui.full_refresh();
1034 };
1035 document.getElementById("toggle_map_mode").onclick = function() {
1036     if (tui.map_mode == 'terrain') {
1037         tui.map_mode = 'control';
1038     } else {
1039         tui.map_mode = 'terrain';
1040     }
1041     tui.full_refresh();
1042 };
1043 document.getElementById("take_thing").onclick = function() {
1044         server.send(['TASK:PICK_UP']);
1045 };
1046 document.getElementById("drop_thing").onclick = function() {
1047         server.send(['TASK:DROP']);
1048 };
1049 document.getElementById("flatten").onclick = function() {
1050     server.send(['TASK:FLATTEN_SURROUNDINGS', tui.password]);
1051 };
1052 document.getElementById("teleport").onclick = function() {
1053     game.teleport();
1054 };
1055 document.getElementById("move_upleft").onclick = function() {
1056     if (tui.mode == mode_play) {
1057         server.send(['TASK:MOVE', 'UPLEFT']);
1058     } else {
1059         explorer.move('UPLEFT');
1060     };
1061 };
1062 document.getElementById("move_left").onclick = function() {
1063     if (tui.mode == mode_play) {
1064         server.send(['TASK:MOVE', 'LEFT']);
1065     } else {
1066         explorer.move('LEFT');
1067     };
1068 };
1069 document.getElementById("move_downleft").onclick = function() {
1070     if (tui.mode == mode_play) {
1071         server.send(['TASK:MOVE', 'DOWNLEFT']);
1072     } else {
1073         explorer.move('DOWNLEFT');
1074     };
1075 };
1076 document.getElementById("move_down").onclick = function() {
1077     if (tui.mode == mode_play) {
1078         server.send(['TASK:MOVE', 'DOWN']);
1079     } else {
1080         explorer.move('DOWN');
1081     };
1082 };
1083 document.getElementById("move_up").onclick = function() {
1084     if (tui.mode == mode_play) {
1085         server.send(['TASK:MOVE', 'UP']);
1086     } else {
1087         explorer.move('UP');
1088     };
1089 };
1090 document.getElementById("move_upright").onclick = function() {
1091     if (tui.mode == mode_play) {
1092         server.send(['TASK:MOVE', 'UPRIGHT']);
1093     } else {
1094         explorer.move('UPRIGHT');
1095     };
1096 };
1097 document.getElementById("move_right").onclick = function() {
1098     if (tui.mode == mode_play) {
1099         server.send(['TASK:MOVE', 'RIGHT']);
1100     } else {
1101         explorer.move('RIGHT');
1102     };
1103 };
1104 document.getElementById("move_downright").onclick = function() {
1105     if (tui.mode == mode_play) {
1106         server.send(['TASK:MOVE', 'DOWNRIGHT']);
1107     } else {
1108         explorer.move('DOWNRIGHT');
1109     };
1110 };
1111 </script>
1112 </body></html>