home · contact · privacy
Add player-identifying meta characters next to @ symbols.
[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] === 'THING_CHAR') {
218             let t = game.get_thing(tokens[1], false);
219             if (t) {
220                 t.player_char = tokens[2];
221             };
222         } else if (tokens[0] === 'TASKS') {
223             game.tasks = tokens[1].split(',')
224         } else if (tokens[0] === 'THING_TYPE') {
225             game.thing_types[tokens[1]] = tokens[2]
226         } else if (tokens[0] === 'MAP') {
227             game.map_geometry = tokens[1];
228             tui.init_keys();
229             game.map_size = parser.parse_yx(tokens[2]);
230             game.map = tokens[3]
231         } else if (tokens[0] === 'FOV') {
232             game.fov = tokens[1]
233         } else if (tokens[0] === 'MAP_CONTROL') {
234             game.map_control = tokens[1]
235         } else if (tokens[0] === 'GAME_STATE_COMPLETE') {
236             game.turn_complete = true;
237             explorer.empty_info_db();
238             if (tui.mode == mode_post_login_wait) {
239                 tui.switch_mode(mode_play);
240             } else if (tui.mode == mode_study) {
241                 explorer.query_info();
242             }
243             let t = game.get_thing(game.player_id);
244             if (t.position in game.portals) {
245                 tui.teleport_target = game.portals[t.position];
246                 tui.switch_mode(mode_teleport);
247                 return;
248             }
249             tui.full_refresh();
250         } else if (tokens[0] === 'CHAT') {
251              tui.log_msg('# ' + tokens[1], 1);
252         } else if (tokens[0] === 'PLAYER_ID') {
253             game.player_id = parseInt(tokens[1]);
254         } else if (tokens[0] === 'LOGIN_OK') {
255             this.send(['GET_GAMESTATE']);
256             tui.switch_mode(mode_post_login_wait);
257         } else if (tokens[0] === 'PORTAL') {
258             let position = parser.parse_yx(tokens[1]);
259             game.portals[position] = tokens[2];
260         } else if (tokens[0] === 'ANNOTATION') {
261             let position = parser.parse_yx(tokens[1]);
262             explorer.update_info_db(position, tokens[2]);
263         } else if (tokens[0] === 'UNHANDLED_INPUT') {
264             tui.log_msg('? unknown command');
265         } else if (tokens[0] === 'PLAY_ERROR') {
266             terminal.blink_screen();
267         } else if (tokens[0] === 'ARGUMENT_ERROR') {
268             tui.log_msg('? syntax error: ' + tokens[1]);
269         } else if (tokens[0] === 'GAME_ERROR') {
270             tui.log_msg('? game error: ' + tokens[1]);
271         } else if (tokens[0] === 'PONG') {
272             ;
273         } else {
274             tui.log_msg('? unhandled input: ' + event.data);
275         }
276     }
277 }
278
279 let unparser = {
280     quote: function(str) {
281         let quoted = ['"'];
282         for (let i = 0; i < str.length; i++) {
283             let c = str[i];
284             if (['"', '\\'].includes(c)) {
285                 quoted.push('\\');
286             };
287             quoted.push(c);
288         }
289         quoted.push('"');
290         return quoted.join('');
291     },
292     to_yx: function(yx_coordinate) {
293         return "Y:" + yx_coordinate[0] + ",X:" + yx_coordinate[1];
294     },
295     untokenize: function(tokens) {
296         let quoted_tokens = [];
297         for (let token of tokens) {
298             quoted_tokens.push(this.quote(token));
299         }
300         return quoted_tokens.join(" ");
301     }
302 }
303
304 class Mode {
305     constructor(name, help_intro, has_input_prompt=false, shows_info=false, is_intro=false) {
306         this.name = name;
307         this.has_input_prompt = has_input_prompt;
308         this.shows_info= shows_info;
309         this.is_intro = is_intro;
310         this.help_intro = help_intro;
311     }
312 }
313 let mode_waiting_for_server = new Mode('waiting_for_server', 'Waiting for a server response.', false, false, true);
314 let mode_login = new Mode('login', 'Pick your player name.', true, false, true);
315 let mode_post_login_wait = new Mode('waiting for game world', 'Waiting for a server response.', false, false, true);
316 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);
317   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);
318 let mode_play = new Mode('play', 'This mode allows you to interact with the map.', false, false);
319 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);
320 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);
321 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);
322 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);
323 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);
324
325 let tui = {
326   mode: mode_waiting_for_server,
327   log: [],
328   input_prompt: '> ',
329   input_lines: [],
330   window_width: terminal.cols / 2,
331   height_turn_line: 1,
332   height_mode_line: 1,
333   height_input: 1,
334   password: 'foo',
335   show_help: false,
336   init: function() {
337       this.inputEl = document.getElementById("input");
338       this.inputEl.focus();
339       this.recalc_input_lines();
340       this.height_header = this.height_turn_line + this.height_mode_line;
341       this.log_msg("@ waiting for server connection ...");
342       this.init_keys();
343   },
344   init_keys: function() {
345     this.keys = {};
346     for (let key_selector of key_selectors) {
347         this.keys[key_selector.id.slice(4)] = key_selector.value;
348     }
349     this.movement_keys = {
350         [this.keys.square_move_up]: 'UP',
351         [this.keys.square_move_left]: 'LEFT',
352         [this.keys.square_move_down]: 'DOWN',
353         [this.keys.square_move_right]: 'RIGHT'
354     };
355     if (game.map_geometry == 'Hex') {
356         this.movement_keys = {
357             [this.keys.hex_move_upleft]: 'UPLEFT',
358             [this.keys.hex_move_upright]: 'UPRIGHT',
359             [this.keys.hex_move_right]: 'RIGHT',
360             [this.keys.hex_move_downright]: 'DOWNRIGHT',
361             [this.keys.hex_move_downleft]: 'DOWNLEFT',
362             [this.keys.hex_move_left]: 'LEFT'
363         };
364     };
365   },
366   switch_mode: function(mode) {
367     this.show_help = false;
368     this.map_mode = 'terrain';
369     if (mode.shows_info && game.player_id in game.things) {
370       explorer.position = game.things[game.player_id].position;
371     }
372     this.mode = mode;
373     this.empty_input();
374     this.restore_input_values();
375     if (mode == mode_login) {
376         if (this.login_name) {
377             server.send(['LOGIN', this.login_name]);
378         } else {
379             this.log_msg("? need login name");
380         }
381     } else if (mode == mode_edit) {
382         this.show_help = true;
383     } else if (mode == mode_teleport) {
384         tui.log_msg("@ May teleport to: " + tui.teleport_target);
385         tui.log_msg("@ Enter 'YES!' to entusiastically affirm.");
386     }
387     this.full_refresh();
388   },
389   restore_input_values: function() {
390       if (this.mode == mode_annotate && explorer.position in explorer.info_db) {
391           let info = explorer.info_db[explorer.position];
392           if (info != "(none)") {
393               this.inputEl.value = info;
394               this.recalc_input_lines();
395           }
396       } else if (this.mode == mode_portal && explorer.position in game.portals) {
397           let portal = game.portals[explorer.position]
398           this.inputEl.value = portal;
399           this.recalc_input_lines();
400       } else if (this.mode == mode_password) {
401           this.inputEl.value = this.password;
402           this.recalc_input_lines();
403       }
404   },
405   empty_input: function(str) {
406       this.inputEl.value = "";
407       if (this.mode.has_input_prompt) {
408           this.recalc_input_lines();
409       } else {
410           this.height_input = 0;
411       }
412   },
413   recalc_input_lines: function() {
414       this.input_lines = this.msg_into_lines_of_width(this.input_prompt + this.inputEl.value, this.window_width);
415       this.height_input = this.input_lines.length;
416   },
417   msg_into_lines_of_width: function(msg, width) {
418     let chunk = "";
419     let lines = [];
420     for (let i = 0, x = 0; i < msg.length; i++, x++) {
421       if (x >= width || msg[i] == "\n") {
422         lines.push(chunk);
423         chunk = "";
424         x = 0;
425       };
426       if (msg[i] != "\n") {
427         chunk += msg[i];
428       }
429     }
430     lines.push(chunk);
431     return lines;
432   },
433   log_msg: function(msg) {
434       this.log.push(msg);
435       while (this.log.length > 100) {
436         this.log.shift();
437       };
438       this.full_refresh();
439   },
440   draw_map: function() {
441     let map_lines_split = [];
442     let line = [];
443     let map_content = game.map;
444     if (this.map_mode == 'control') {
445         map_content = game.map_control;
446     }
447     for (let i = 0, j = 0; i < game.map.length; i++, j++) {
448         if (j == game.map_size[1]) {
449             map_lines_split.push(line);
450             line = [];
451             j = 0;
452         };
453         line.push(map_content[i] + ' ');
454     };
455     map_lines_split.push(line);
456     if (this.map_mode == 'terrain') {
457         let used_positions = [];
458         for (const thing_id in game.things) {
459             let t = game.things[thing_id];
460             let symbol = game.thing_types[t.type_];
461             let meta_char = ' ';
462             if (t.player_char) {
463                 meta_char = t.player_char;
464             }
465             if (used_positions.includes(t.position.toString())) {
466                 meta_char = '+';
467             };
468             map_lines_split[t.position[0]][t.position[1]] = symbol + meta_char;
469             used_positions.push(t.position.toString());
470         };
471     }
472     if (tui.mode.shows_info) {
473         map_lines_split[explorer.position[0]][explorer.position[1]] = '??';
474     }
475     let map_lines = []
476     if (game.map_geometry == 'Square') {
477         for (let line_split of map_lines_split) {
478             map_lines.push(line_split.join(''));
479         };
480     } else if (game.map_geometry == 'Hex') {
481         let indent = 0
482         for (let line_split of map_lines_split) {
483             map_lines.push(' '.repeat(indent) + line_split.join(''));
484             if (indent == 0) {
485                 indent = 1;
486             } else {
487                 indent = 0;
488             };
489         };
490     }
491     let window_center = [terminal.rows / 2, this.window_width / 2];
492     let player = game.things[game.player_id];
493     let center_position = [player.position[0], player.position[1]];
494     if (tui.mode.shows_info) {
495         center_position = [explorer.position[0], explorer.position[1]];
496     }
497     center_position[1] = center_position[1] * 2;
498     let offset = [center_position[0] - window_center[0],
499                   center_position[1] - window_center[1]]
500     if (game.map_geometry == 'Hex' && offset[0] % 2) {
501         offset[1] += 1;
502     };
503     let term_y = Math.max(0, -offset[0]);
504     let term_x = Math.max(0, -offset[1]);
505     let map_y = Math.max(0, offset[0]);
506     let map_x = Math.max(0, offset[1]);
507     for (; term_y < terminal.rows && map_y < game.map_size[0]; term_y++, map_y++) {
508         let to_draw = map_lines[map_y].slice(map_x, this.window_width + offset[1]);
509         terminal.write(term_y, term_x, to_draw);
510     }
511   },
512   draw_mode_line: function() {
513       let help = 'hit [' + this.keys.help + '] for help';
514       if (this.mode.has_input_prompt) {
515           help = 'enter /help for help';
516       }
517       terminal.write(0, this.window_width, 'MODE: ' + this.mode.name + ' – ' + help);
518   },
519   draw_turn_line: function(n) {
520     terminal.write(1, this.window_width, 'TURN: ' + game.turn);
521   },
522   draw_history: function() {
523       let log_display_lines = [];
524       for (let line of this.log) {
525           log_display_lines = log_display_lines.concat(this.msg_into_lines_of_width(line, this.window_width));
526       };
527       for (let y = terminal.rows - 1 - this.height_input,
528                i = log_display_lines.length - 1;
529            y >= this.height_header && i >= 0;
530            y--, i--) {
531           terminal.write(y, this.window_width, log_display_lines[i]);
532       }
533   },
534   draw_info: function() {
535     let lines = this.msg_into_lines_of_width(explorer.get_info(), this.window_width);
536     for (let y = this.height_header, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
537       terminal.write(y, this.window_width, lines[i]);
538     }
539   },
540   draw_input: function() {
541     if (this.mode.has_input_prompt) {
542         for (let y = terminal.rows - this.height_input, i = 0; i < this.input_lines.length; y++, i++) {
543             terminal.write(y, this.window_width, this.input_lines[i]);
544         }
545     }
546   },
547   draw_help: function() {
548       let movement_keys_desc = Object.keys(this.movement_keys).join(',');
549       let content = this.mode.name + " mode help\n\n" + this.mode.help_intro + "\n\n";
550       if (this.mode == mode_play) {
551           content += "Available actions:\n";
552           if (game.tasks.includes('MOVE')) {
553               content += "[" + movement_keys_desc + "] – move player\n";
554           }
555           if (game.tasks.includes('PICK_UP')) {
556               content += "[" + this.keys.take_thing + "] – take thing under player\n";
557           }
558           if (game.tasks.includes('DROP')) {
559               content += "[" + this.keys.drop_thing + "] – drop carried thing\n";
560           }
561           if (game.tasks.includes('FLATTEN_SURROUNDINGS')) {
562               content += "[" + tui.keys.flatten + "] – flatten player's surroundings\n";
563           }
564           content += '\nOther modes available from here:\n';
565           content += '[' + this.keys.switch_to_chat + '] – chat mode\n';
566           content += '[' + this.keys.switch_to_study + '] – study mode\n';
567           content += '[' + this.keys.switch_to_edit + '] – terrain edit mode\n';
568           content += '[' + this.keys.switch_to_portal + '] – portal edit mode\n';
569           content += '[' + this.keys.switch_to_annotate + '] – annotation mode\n';
570           content += '[' + this.keys.switch_to_password + '] – password input mode\n';
571       } else if (this.mode == mode_study) {
572           content += "Available actions:\n";
573           content += '[' + movement_keys_desc + '] – move question mark\n';
574           content += '[' + this.keys.toggle_map_mode + '] – toggle view between terrain, and password protection areas\n';
575           content += '\nOther modes available from here:\n';
576           content += '[' + this.keys.switch_to_chat + '] – chat mode\n';
577           content += '[' + this.keys.switch_to_play + '] – play mode\n';
578       } else if (this.mode == mode_chat) {
579           content += '/nick NAME – re-name yourself to NAME\n';
580           //content += '/msg USER TEXT – send TEXT to USER\n';
581           content += '/' + this.keys.switch_to_play + ' or /play – switch to play mode\n';
582           content += '/' + this.keys.switch_to_study + ' or /study – switch to study mode\n';
583       }
584       let start_x = 0;
585       if (!this.mode.has_input_prompt) {
586           start_x = this.window_width
587       }
588       terminal.drawBox(0, start_x, terminal.rows, this.window_width);
589       let lines = this.msg_into_lines_of_width(content, this.window_width);
590       for (let y = 0, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
591           terminal.write(y, start_x, lines[i]);
592       }
593   },
594   full_refresh: function() {
595     terminal.drawBox(0, 0, terminal.rows, terminal.cols);
596     if (this.mode.is_intro) {
597         this.draw_history();
598         this.draw_input();
599     } else {
600         if (game.turn_complete) {
601             this.draw_map();
602             this.draw_turn_line();
603         }
604         this.draw_mode_line();
605         if (this.mode.shows_info) {
606           this.draw_info();
607         } else {
608           this.draw_history();
609         }
610         this.draw_input();
611     }
612     if (this.show_help) {
613         this.draw_help();
614     }
615     terminal.refresh();
616   }
617 }
618
619 let game = {
620     init: function() {
621         this.things = {};
622         this.turn = -1;
623         this.map = "";
624         this.map_control = "";
625         this.map_size = [0,0];
626         this.player_id = -1;
627         this.portals = {};
628         this.tasks = {};
629     },
630     get_thing: function(id_, create_if_not_found=false) {
631         if (id_ in game.things) {
632             return game.things[id_];
633         } else if (create_if_not_found) {
634             let t = new Thing([0,0]);
635             game.things[id_] = t;
636             return t;
637         };
638     },
639     move: function(start_position, direction) {
640         let target = [start_position[0], start_position[1]];
641         if (direction == 'LEFT') {
642             target[1] -= 1;
643         } else if (direction == 'RIGHT') {
644             target[1] += 1;
645         } else if (game.map_geometry == 'Square') {
646             if (direction == 'UP') {
647                 target[0] -= 1;
648             } else if (direction == 'DOWN') {
649                 target[0] += 1;
650             };
651         } else if (game.map_geometry == 'Hex') {
652             let start_indented = start_position[0] % 2;
653             if (direction == 'UPLEFT') {
654                 target[0] -= 1;
655                 if (!start_indented) {
656                     target[1] -= 1;
657                 }
658             } else if (direction == 'UPRIGHT') {
659                 target[0] -= 1;
660                 if (start_indented) {
661                     target[1] += 1;
662                 }
663             } else if (direction == 'DOWNLEFT') {
664                 target[0] += 1;
665                 if (!start_indented) {
666                     target[1] -= 1;
667                 }
668             } else if (direction == 'DOWNRIGHT') {
669                 target[0] += 1;
670                 if (start_indented) {
671                     target[1] += 1;
672                 }
673             };
674         };
675         if (target[0] < 0 || target[1] < 0 ||
676             target[0] >= this.map_size[0] || target[1] >= this.map_size[1]) {
677             return null;
678         };
679         return target;
680     }
681 }
682
683 game.init();
684 tui.init();
685 tui.full_refresh();
686 server.init(websocket_location);
687
688 let explorer = {
689     position: [0,0],
690     info_db: {},
691     move: function(direction) {
692         let target = game.move(this.position, direction);
693         if (target) {
694             this.position = target
695             this.query_info();
696         } else {
697             terminal.blink_screen();
698         };
699     },
700     update_info_db: function(yx, str) {
701         this.info_db[yx] = str;
702         if (tui.mode == mode_study) {
703             tui.full_refresh();
704         }
705     },
706     empty_info_db: function() {
707         this.info_db = {};
708         if (tui.mode == mode_study) {
709             tui.full_refresh();
710         }
711     },
712     query_info: function() {
713         server.send(["GET_ANNOTATION", unparser.to_yx(explorer.position)]);
714     },
715     get_info: function() {
716         let position_i = this.position[0] * game.map_size[1] + this.position[1];
717         if (game.fov[position_i] != '.') {
718             return 'outside field of view';
719         };
720         let info = "";
721         info += "TERRAIN: " + game.map[position_i] + "\n";
722         for (let t_id in game.things) {
723              let t = game.things[t_id];
724              if (t.position[0] == this.position[0] && t.position[1] == this.position[1]) {
725                  let symbol = game.thing_types[t.type_];
726                  info += "THING: " + t.type_ + " / " + symbol;
727                  if (t.player_char) {
728                      info += t.player_char;
729                  };
730                  if (t.name_) {
731                      info += " (name: " + t.name_ + ")";
732                  }
733                  info += "\n";
734              }
735         }
736         if (this.position in game.portals) {
737             info += "PORTAL: " + game.portals[this.position] + "\n";
738         }
739         if (this.position in this.info_db) {
740             info += "ANNOTATIONS: " + this.info_db[this.position];
741         } else {
742             info += 'waiting …';
743         }
744         return info;
745     },
746     annotate: function(msg) {
747         if (msg.length == 0) {
748             msg = " ";  // triggers annotation deletion
749         }
750         server.send(["ANNOTATE", unparser.to_yx(explorer.position), msg, tui.password]);
751     },
752     set_portal: function(msg) {
753         if (msg.length == 0) {
754             msg = " ";  // triggers portal deletion
755         }
756         server.send(["PORTAL", unparser.to_yx(explorer.position), msg, tui.password]);
757     }
758 }
759
760 tui.inputEl.addEventListener('input', (event) => {
761     if (tui.mode.has_input_prompt) {
762         let max_length = tui.window_width * terminal.rows - tui.input_prompt.length;
763         if (tui.inputEl.value.length > max_length) {
764             tui.inputEl.value = tui.inputEl.value.slice(0, max_length);
765         };
766         tui.recalc_input_lines();
767     } else if (tui.mode == mode_edit && tui.inputEl.value.length > 0) {
768         server.send(["TASK:WRITE", tui.inputEl.value[0], tui.password]);
769         tui.switch_mode(mode_play);
770     }
771     tui.full_refresh();
772 }, false);
773 tui.inputEl.addEventListener('keydown', (event) => {
774     tui.show_help = false;
775     if (event.key == 'Enter') {
776         event.preventDefault();
777     }
778     if (tui.mode.has_input_prompt && event.key == 'Enter' && tui.inputEl.value == '/help') {
779         tui.show_help = true;
780         tui.empty_input();
781         tui.restore_input_values();
782     } else if (!tui.mode.has_input_prompt && event.key == tui.keys.help) {
783         tui.show_help = true;
784     } else if (tui.mode == mode_login && event.key == 'Enter') {
785         tui.login_name = tui.inputEl.value;
786         server.send(['LOGIN', tui.inputEl.value]);
787         tui.empty_input();
788     } else if (tui.mode == mode_portal && event.key == 'Enter') {
789         explorer.set_portal(tui.inputEl.value);
790         tui.switch_mode(mode_play);
791     } else if (tui.mode == mode_annotate && event.key == 'Enter') {
792         explorer.annotate(tui.inputEl.value);
793         tui.switch_mode(mode_play);
794     } else if (tui.mode == mode_password && event.key == 'Enter') {
795         if (tui.inputEl.value.length == 0) {
796             tui.inputEl.value = " ";
797         }
798         tui.password = tui.inputEl.value
799         tui.switch_mode(mode_play);
800     } else if (tui.mode == mode_teleport && event.key == 'Enter') {
801         if (tui.inputEl.value == 'YES!') {
802             server.reconnect_to(tui.teleport_target);
803         } else {
804             tui.log_msg('@ teleport aborted');
805             tui.switch_mode(mode_play);
806         };
807     } else if (tui.mode == mode_chat && event.key == 'Enter') {
808         let [tokens, token_starts] = parser.tokenize(tui.inputEl.value);
809         if (tokens.length > 0 && tokens[0].length > 0) {
810             if (tui.inputEl.value[0][0] == '/') {
811                 if (tokens[0].slice(1) == 'play' || tokens[0][1] == tui.keys.switch_to_play) {
812                     tui.switch_mode(mode_play);
813                 } else if (tokens[0].slice(1) == 'study' || tokens[0][1] == tui.keys.switch_to_study) {
814                     tui.switch_mode(mode_study);
815                 } else if (tokens[0].slice(1) == 'nick') {
816                     if (tokens.length > 1) {
817                         server.send(['NICK', tokens[1]]);
818                     } else {
819                         tui.log_msg('? need new name');
820                     }
821                 //} else if (tokens[0].slice(1) == 'msg') {
822                 //    if (tokens.length > 2) {
823                 //        let msg = tui.inputEl.value.slice(token_starts[2]);
824                 //        server.send(['QUERY', tokens[1], msg]);
825                 //    } else {
826                 //        tui.log_msg('? need message target and message');
827                 //    }
828                 } else {
829                     tui.log_msg('? unknown command');
830                 }
831             } else {
832                     server.send(['ALL', tui.inputEl.value]);
833             }
834         } else if (tui.inputEl.valuelength > 0) {
835                 server.send(['ALL', tui.inputEl.value]);
836         }
837         tui.empty_input();
838     } else if (tui.mode == mode_play) {
839           if (event.key === tui.keys.switch_to_chat) {
840               event.preventDefault();
841               tui.switch_mode(mode_chat);
842           } else if (event.key === tui.keys.switch_to_edit
843                      && game.tasks.includes('WRITE')) {
844               event.preventDefault();
845               tui.switch_mode(mode_edit);
846           } else if (event.key === tui.keys.switch_to_study) {
847               tui.switch_mode(mode_study);
848           } else if (event.key === tui.keys.switch_to_password) {
849               event.preventDefault();
850               tui.switch_mode(mode_password);
851           } else if (event.key === tui.keys.flatten
852                      && game.tasks.includes('FLATTEN_SURROUNDINGS')) {
853               server.send(["TASK:FLATTEN_SURROUNDINGS", tui.password]);
854           } else if (event.key === tui.keys.take_thing
855                      && game.tasks.includes('PICK_UP')) {
856               server.send(["TASK:PICK_UP"]);
857           } else if (event.key === tui.keys.drop_thing
858                      && game.tasks.includes('DROP')) {
859               server.send(["TASK:DROP"]);
860           } else if (event.key in tui.movement_keys
861                      && game.tasks.includes('MOVE')) {
862               server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
863           } else if (event.key === tui.keys.switch_to_portal) {
864               event.preventDefault();
865               tui.switch_mode(mode_portal);
866           } else if (event.key === tui.keys.switch_to_annotate) {
867               event.preventDefault();
868               tui.switch_mode(mode_annotate);
869           };
870     } else if (tui.mode == mode_study) {
871         if (event.key === tui.keys.switch_to_chat) {
872             event.preventDefault();
873             tui.switch_mode(mode_chat);
874         } else if (event.key == tui.keys.switch_to_play) {
875             tui.switch_mode(mode_play);
876         } else if (event.key in tui.movement_keys) {
877             explorer.move(tui.movement_keys[event.key]);
878         } else if (event.key == tui.keys.toggle_map_mode) {
879             if (tui.map_mode == 'terrain') {
880                 tui.map_mode = 'control';
881             } else {
882                 tui.map_mode = 'terrain';
883             }
884         };
885     }
886     tui.full_refresh();
887 }, false);
888
889 rows_selector.addEventListener('input', function() {
890     if (rows_selector.value % 4 != 0) {
891         return;
892     }
893     window.localStorage.setItem(rows_selector.id, rows_selector.value);
894     terminal.initialize();
895     tui.full_refresh();
896 }, false);
897 cols_selector.addEventListener('input', function() {
898     if (cols_selector.value % 4 != 0) {
899         return;
900     }
901     window.localStorage.setItem(cols_selector.id, cols_selector.value);
902     terminal.initialize();
903     tui.window_width = terminal.cols / 2,
904     tui.full_refresh();
905 }, false);
906 for (let key_selector of key_selectors) {
907     key_selector.addEventListener('input', function() {
908         window.localStorage.setItem(key_selector.id, key_selector.value);
909         tui.init_keys();
910     }, false);
911 }
912 window.setInterval(function() {
913     if (!(['input', 'n_cols', 'n_rows'].includes(document.activeElement.id)
914           || document.activeElement.id.startsWith('key_'))) {
915         tui.inputEl.focus();
916     }
917 }, 100);
918 window.setInterval(function() {
919     if (server.connected) {
920         server.send(['PING']);
921     } else {
922         server.reconnect_to(server.url);
923         tui.log_msg('@ attempting reconnect …')
924     }
925 }, 5000);
926 </script>
927 </body></html>