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