home · contact · privacy
Augment map display with thing amount info.
[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         let used_positions = [];
454         for (const thing_id in game.things) {
455             let t = game.things[thing_id];
456             let symbol = game.thing_types[t.type_];
457             if (used_positions.includes(t.position.toString())) {
458                 map_lines_split[t.position[0]][t.position[1]] = symbol + '+';
459             } else {
460                 map_lines_split[t.position[0]][t.position[1]] = symbol + ' ';
461             };
462             used_positions.push(t.position.toString());
463         };
464     }
465     if (tui.mode.shows_info) {
466         map_lines_split[explorer.position[0]][explorer.position[1]] = '??';
467     }
468     let map_lines = []
469     if (game.map_geometry == 'Square') {
470         for (let line_split of map_lines_split) {
471             map_lines.push(line_split.join(' '));
472         };
473     } else if (game.map_geometry == 'Hex') {
474         let indent = 0
475         for (let line_split of map_lines_split) {
476             map_lines.push(' '.repeat(indent) + line_split.join(''));
477             if (indent == 0) {
478                 indent = 1;
479             } else {
480                 indent = 0;
481             };
482         };
483     }
484     let window_center = [terminal.rows / 2, this.window_width / 2];
485     let player = game.things[game.player_id];
486     let center_position = [player.position[0], player.position[1]];
487     if (tui.mode.shows_info) {
488         center_position = [explorer.position[0], explorer.position[1]];
489     }
490     center_position[1] = center_position[1] * 2;
491     let offset = [center_position[0] - window_center[0],
492                   center_position[1] - window_center[1]]
493     if (game.map_geometry == 'Hex' && offset[0] % 2) {
494         offset[1] += 1;
495     };
496     let term_y = Math.max(0, -offset[0]);
497     let term_x = Math.max(0, -offset[1]);
498     let map_y = Math.max(0, offset[0]);
499     let map_x = Math.max(0, offset[1]);
500     for (; term_y < terminal.rows && map_y < game.map_size[0]; term_y++, map_y++) {
501         let to_draw = map_lines[map_y].slice(map_x, this.window_width + offset[1]);
502         terminal.write(term_y, term_x, to_draw);
503     }
504   },
505   draw_mode_line: function() {
506       let help = 'hit [' + this.keys.help + '] for help';
507       if (this.mode.has_input_prompt) {
508           help = 'enter /help for help';
509       }
510       terminal.write(0, this.window_width, 'MODE: ' + this.mode.name + ' – ' + help);
511   },
512   draw_turn_line: function(n) {
513     terminal.write(1, this.window_width, 'TURN: ' + game.turn);
514   },
515   draw_history: function() {
516       let log_display_lines = [];
517       for (let line of this.log) {
518           log_display_lines = log_display_lines.concat(this.msg_into_lines_of_width(line, this.window_width));
519       };
520       for (let y = terminal.rows - 1 - this.height_input,
521                i = log_display_lines.length - 1;
522            y >= this.height_header && i >= 0;
523            y--, i--) {
524           terminal.write(y, this.window_width, log_display_lines[i]);
525       }
526   },
527   draw_info: function() {
528     let lines = this.msg_into_lines_of_width(explorer.get_info(), this.window_width);
529     for (let y = this.height_header, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
530       terminal.write(y, this.window_width, lines[i]);
531     }
532   },
533   draw_input: function() {
534     if (this.mode.has_input_prompt) {
535         for (let y = terminal.rows - this.height_input, i = 0; i < this.input_lines.length; y++, i++) {
536             terminal.write(y, this.window_width, this.input_lines[i]);
537         }
538     }
539   },
540   draw_help: function() {
541       let movement_keys_desc = Object.keys(this.movement_keys).join(',');
542       let content = this.mode.name + " mode help\n\n" + this.mode.help_intro + "\n\n";
543       if (this.mode == mode_play) {
544           content += "Available actions:\n";
545           if (game.tasks.includes('MOVE')) {
546               content += "[" + movement_keys_desc + "] – move player\n";
547           }
548           if (game.tasks.includes('PICK_UP')) {
549               content += "[" + this.keys.take_thing + "] – take thing under player\n";
550           }
551           if (game.tasks.includes('DROP')) {
552               content += "[" + this.keys.drop_thing + "] – drop carried thing\n";
553           }
554           if (game.tasks.includes('FLATTEN_SURROUNDINGS')) {
555               content += "[" + tui.keys.flatten + "] – flatten player's surroundings\n";
556           }
557           content += '\nOther modes available from here:\n';
558           content += '[' + this.keys.switch_to_chat + '] – chat mode\n';
559           content += '[' + this.keys.switch_to_study + '] – study mode\n';
560           content += '[' + this.keys.switch_to_edit + '] – terrain edit mode\n';
561           content += '[' + this.keys.switch_to_portal + '] – portal edit mode\n';
562           content += '[' + this.keys.switch_to_annotate + '] – annotation mode\n';
563           content += '[' + this.keys.switch_to_password + '] – password input mode\n';
564       } else if (this.mode == mode_study) {
565           content += "Available actions:\n";
566           content += '[' + movement_keys_desc + '] – move question mark\n';
567           content += '[' + this.keys.toggle_map_mode + '] – toggle view between terrain, and password protection areas\n';
568           content += '\nOther modes available from here:\n';
569           content += '[' + this.keys.switch_to_chat + '] – chat mode\n';
570           content += '[' + this.keys.switch_to_play + '] – play mode\n';
571       } else if (this.mode == mode_chat) {
572           content += '/nick NAME – re-name yourself to NAME\n';
573           //content += '/msg USER TEXT – send TEXT to USER\n';
574           content += '/' + this.keys.switch_to_play + ' or /play – switch to play mode\n';
575           content += '/' + this.keys.switch_to_study + ' or /study – switch to study mode\n';
576       }
577       let start_x = 0;
578       if (!this.mode.has_input_prompt) {
579           start_x = this.window_width
580       }
581       terminal.drawBox(0, start_x, terminal.rows, this.window_width);
582       let lines = this.msg_into_lines_of_width(content, this.window_width);
583       for (let y = 0, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
584           terminal.write(y, start_x, lines[i]);
585       }
586   },
587   full_refresh: function() {
588     terminal.drawBox(0, 0, terminal.rows, terminal.cols);
589     if (this.mode.is_intro) {
590         this.draw_history();
591         this.draw_input();
592     } else {
593         if (game.turn_complete) {
594             this.draw_map();
595             this.draw_turn_line();
596         }
597         this.draw_mode_line();
598         if (this.mode.shows_info) {
599           this.draw_info();
600         } else {
601           this.draw_history();
602         }
603         this.draw_input();
604     }
605     if (this.show_help) {
606         this.draw_help();
607     }
608     terminal.refresh();
609   }
610 }
611
612 let game = {
613     init: function() {
614         this.things = {};
615         this.turn = -1;
616         this.map = "";
617         this.map_control = "";
618         this.map_size = [0,0];
619         this.player_id = -1;
620         this.portals = {};
621         this.tasks = {};
622     },
623     get_thing: function(id_, create_if_not_found=false) {
624         if (id_ in game.things) {
625             return game.things[id_];
626         } else if (create_if_not_found) {
627             let t = new Thing([0,0]);
628             game.things[id_] = t;
629             return t;
630         };
631     },
632     move: function(start_position, direction) {
633         let target = [start_position[0], start_position[1]];
634         if (direction == 'LEFT') {
635             target[1] -= 1;
636         } else if (direction == 'RIGHT') {
637             target[1] += 1;
638         } else if (game.map_geometry == 'Square') {
639             if (direction == 'UP') {
640                 target[0] -= 1;
641             } else if (direction == 'DOWN') {
642                 target[0] += 1;
643             };
644         } else if (game.map_geometry == 'Hex') {
645             let start_indented = start_position[0] % 2;
646             if (direction == 'UPLEFT') {
647                 target[0] -= 1;
648                 if (!start_indented) {
649                     target[1] -= 1;
650                 }
651             } else if (direction == 'UPRIGHT') {
652                 target[0] -= 1;
653                 if (start_indented) {
654                     target[1] += 1;
655                 }
656             } else if (direction == 'DOWNLEFT') {
657                 target[0] += 1;
658                 if (!start_indented) {
659                     target[1] -= 1;
660                 }
661             } else if (direction == 'DOWNRIGHT') {
662                 target[0] += 1;
663                 if (start_indented) {
664                     target[1] += 1;
665                 }
666             };
667         };
668         if (target[0] < 0 || target[1] < 0 ||
669             target[0] >= this.map_size[0] || target[1] >= this.map_size[1]) {
670             return null;
671         };
672         return target;
673     }
674 }
675
676 game.init();
677 tui.init();
678 tui.full_refresh();
679 server.init(websocket_location);
680
681 let explorer = {
682     position: [0,0],
683     info_db: {},
684     move: function(direction) {
685         let target = game.move(this.position, direction);
686         if (target) {
687             this.position = target
688             this.query_info();
689         } else {
690             terminal.blink_screen();
691         };
692     },
693     update_info_db: function(yx, str) {
694         this.info_db[yx] = str;
695         if (tui.mode == mode_study) {
696             tui.full_refresh();
697         }
698     },
699     empty_info_db: function() {
700         this.info_db = {};
701         if (tui.mode == mode_study) {
702             tui.full_refresh();
703         }
704     },
705     query_info: function() {
706         server.send(["GET_ANNOTATION", unparser.to_yx(explorer.position)]);
707     },
708     get_info: function() {
709         let position_i = this.position[0] * game.map_size[1] + this.position[1];
710         if (game.fov[position_i] != '.') {
711             return 'outside field of view';
712         };
713         let info = "";
714         info += "TERRAIN: " + game.map[position_i] + "\n";
715         for (let t_id in game.things) {
716              let t = game.things[t_id];
717              if (t.position[0] == this.position[0] && t.position[1] == this.position[1]) {
718                  info += "THING: " + t.type_;
719                  if (t.name_) {
720                      info += " (name: " + t.name_ + ")";
721                  }
722                  info += "\n";
723              }
724         }
725         if (this.position in game.portals) {
726             info += "PORTAL: " + game.portals[this.position] + "\n";
727         }
728         if (this.position in this.info_db) {
729             info += "ANNOTATIONS: " + this.info_db[this.position];
730         } else {
731             info += 'waiting …';
732         }
733         return info;
734     },
735     annotate: function(msg) {
736         if (msg.length == 0) {
737             msg = " ";  // triggers annotation deletion
738         }
739         server.send(["ANNOTATE", unparser.to_yx(explorer.position), msg, tui.password]);
740     },
741     set_portal: function(msg) {
742         if (msg.length == 0) {
743             msg = " ";  // triggers portal deletion
744         }
745         server.send(["PORTAL", unparser.to_yx(explorer.position), msg, tui.password]);
746     }
747 }
748
749 tui.inputEl.addEventListener('input', (event) => {
750     if (tui.mode.has_input_prompt) {
751         let max_length = tui.window_width * terminal.rows - tui.input_prompt.length;
752         if (tui.inputEl.value.length > max_length) {
753             tui.inputEl.value = tui.inputEl.value.slice(0, max_length);
754         };
755         tui.recalc_input_lines();
756     } else if (tui.mode == mode_edit && tui.inputEl.value.length > 0) {
757         server.send(["TASK:WRITE", tui.inputEl.value[0], tui.password]);
758         tui.switch_mode(mode_play);
759     }
760     tui.full_refresh();
761 }, false);
762 tui.inputEl.addEventListener('keydown', (event) => {
763     tui.show_help = false;
764     if (event.key == 'Enter') {
765         event.preventDefault();
766     }
767     if (tui.mode.has_input_prompt && event.key == 'Enter' && tui.inputEl.value == '/help') {
768         tui.show_help = true;
769         tui.empty_input();
770         tui.restore_input_values();
771     } else if (!tui.mode.has_input_prompt && event.key == tui.keys.help) {
772         tui.show_help = true;
773     } else if (tui.mode == mode_login && event.key == 'Enter') {
774         tui.login_name = tui.inputEl.value;
775         server.send(['LOGIN', tui.inputEl.value]);
776         tui.empty_input();
777     } else if (tui.mode == mode_portal && event.key == 'Enter') {
778         explorer.set_portal(tui.inputEl.value);
779         tui.switch_mode(mode_play);
780     } else if (tui.mode == mode_annotate && event.key == 'Enter') {
781         explorer.annotate(tui.inputEl.value);
782         tui.switch_mode(mode_play);
783     } else if (tui.mode == mode_password && event.key == 'Enter') {
784         if (tui.inputEl.value.length == 0) {
785             tui.inputEl.value = " ";
786         }
787         tui.password = tui.inputEl.value
788         tui.switch_mode(mode_play);
789     } else if (tui.mode == mode_teleport && event.key == 'Enter') {
790         if (tui.inputEl.value == 'YES!') {
791             server.reconnect_to(tui.teleport_target);
792         } else {
793             tui.log_msg('@ teleport aborted');
794             tui.switch_mode(mode_play);
795         };
796     } else if (tui.mode == mode_chat && event.key == 'Enter') {
797         let [tokens, token_starts] = parser.tokenize(tui.inputEl.value);
798         if (tokens.length > 0 && tokens[0].length > 0) {
799             if (tui.inputEl.value[0][0] == '/') {
800                 if (tokens[0].slice(1) == 'play' || tokens[0][1] == tui.keys.switch_to_play) {
801                     tui.switch_mode(mode_play);
802                 } else if (tokens[0].slice(1) == 'study' || tokens[0][1] == tui.keys.switch_to_study) {
803                     tui.switch_mode(mode_study);
804                 } else if (tokens[0].slice(1) == 'nick') {
805                     if (tokens.length > 1) {
806                         server.send(['NICK', tokens[1]]);
807                     } else {
808                         tui.log_msg('? need new name');
809                     }
810                 //} else if (tokens[0].slice(1) == 'msg') {
811                 //    if (tokens.length > 2) {
812                 //        let msg = tui.inputEl.value.slice(token_starts[2]);
813                 //        server.send(['QUERY', tokens[1], msg]);
814                 //    } else {
815                 //        tui.log_msg('? need message target and message');
816                 //    }
817                 } else {
818                     tui.log_msg('? unknown command');
819                 }
820             } else {
821                     server.send(['ALL', tui.inputEl.value]);
822             }
823         } else if (tui.inputEl.valuelength > 0) {
824                 server.send(['ALL', tui.inputEl.value]);
825         }
826         tui.empty_input();
827     } else if (tui.mode == mode_play) {
828           if (event.key === tui.keys.switch_to_chat) {
829               event.preventDefault();
830               tui.switch_mode(mode_chat);
831           } else if (event.key === tui.keys.switch_to_edit
832                      && game.tasks.includes('WRITE')) {
833               event.preventDefault();
834               tui.switch_mode(mode_edit);
835           } else if (event.key === tui.keys.switch_to_study) {
836               tui.switch_mode(mode_study);
837           } else if (event.key === tui.keys.switch_to_password) {
838               event.preventDefault();
839               tui.switch_mode(mode_password);
840           } else if (event.key === tui.keys.flatten
841                      && game.tasks.includes('FLATTEN_SURROUNDINGS')) {
842               server.send(["TASK:FLATTEN_SURROUNDINGS", tui.password]);
843           } else if (event.key === tui.keys.take_thing
844                      && game.tasks.includes('PICK_UP')) {
845               server.send(["TASK:PICK_UP"]);
846           } else if (event.key === tui.keys.drop_thing
847                      && game.tasks.includes('DROP')) {
848               server.send(["TASK:DROP"]);
849           } else if (event.key in tui.movement_keys
850                      && game.tasks.includes('MOVE')) {
851               server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
852           } else if (event.key === tui.keys.switch_to_portal) {
853               event.preventDefault();
854               tui.switch_mode(mode_portal);
855           } else if (event.key === tui.keys.switch_to_annotate) {
856               event.preventDefault();
857               tui.switch_mode(mode_annotate);
858           };
859     } else if (tui.mode == mode_study) {
860         if (event.key === tui.keys.switch_to_chat) {
861             event.preventDefault();
862             tui.switch_mode(mode_chat);
863         } else if (event.key == tui.keys.switch_to_play) {
864             tui.switch_mode(mode_play);
865         } else if (event.key in tui.movement_keys) {
866             explorer.move(tui.movement_keys[event.key]);
867         } else if (event.key == tui.keys.toggle_map_mode) {
868             if (tui.map_mode == 'terrain') {
869                 tui.map_mode = 'control';
870             } else {
871                 tui.map_mode = 'terrain';
872             }
873         };
874     }
875     tui.full_refresh();
876 }, false);
877
878 rows_selector.addEventListener('input', function() {
879     if (rows_selector.value % 4 != 0) {
880         return;
881     }
882     window.localStorage.setItem(rows_selector.id, rows_selector.value);
883     terminal.initialize();
884     tui.full_refresh();
885 }, false);
886 cols_selector.addEventListener('input', function() {
887     if (cols_selector.value % 4 != 0) {
888         return;
889     }
890     window.localStorage.setItem(cols_selector.id, cols_selector.value);
891     terminal.initialize();
892     tui.window_width = terminal.cols / 2,
893     tui.full_refresh();
894 }, false);
895 for (let key_selector of key_selectors) {
896     key_selector.addEventListener('input', function() {
897         window.localStorage.setItem(key_selector.id, key_selector.value);
898         tui.init_keys();
899     }, false);
900 }
901 window.setInterval(function() {
902     if (!(['input', 'n_cols', 'n_rows'].includes(document.activeElement.id)
903           || document.activeElement.id.startsWith('key_'))) {
904         tui.inputEl.focus();
905     }
906 }, 100);
907 window.setInterval(function() {
908     if (server.connected) {
909         server.send(['PING']);
910     } else {
911         server.reconnect_to(server.url);
912         tui.log_msg('@ attempting reconnect …')
913     }
914 }, 5000);
915 </script>
916 </body></html>