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