home · contact · privacy
7928e2412f8b6c2acfd3006dad58bb0c164a0e95
[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=24 value=24 />
8 terminal columns: <input id="n_cols" type="number" step=4 min=80 value=80 />
9 </div>
10 <pre id="terminal" style="display: inline-block;"></pre>
11 <textarea id="input" style="opacity: 0; width: 0px;"></textarea>
12 <div>
13 <h3>for mouse players</h3>
14 <table style="float: left">
15 <tr><td><button id="move_upleft">up-left</button></td><td><button id="move_up">up</button></td><td><button id="move_upright">up-right</button></td></tr>
16 <tr><td><button id="move_left">left</button></td><td>MOVE</td><td><button id="move_right">right</button></td></tr>
17 <tr><td><button id="move_downleft">down-left</button></td><td><button id="move_down">down</button></td><td><button id="move_downright">down-right</button></td></tr>
18 </table>
19 <div>
20 <button id="help">help</button>
21 <button id="switch_to_play">play mode</button>
22 <button id="switch_to_study">study mode</button>
23 <button id="switch_to_chat">chat mode</button><br />
24 <button id="take_thing">take thing</button>
25 <button id="drop_thing">drop thing</button>
26 <button id="flatten">flatten surroundings</button>
27 <button id="teleport">teleport</button>
28 <button id="switch_to_edit">change tile</button><br />
29 <button id="switch_to_password">change tile editing password</button>
30 <button id="switch_to_annotate">annotate tile</button>
31 <button id="switch_to_portal">edit portal link</button>
32 <button id="toggle_map_mode">toggle terrain/annotations/control view</button>
33 <button id="switch_to_admin">become admin</button>
34 <button id="switch_to_control_pw_type">change tile control password</button>
35 </div>
36 <h3>edit keybindings</h3> (see <a href="https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values">here</a> for non-obvious available values):<br />
37 <ul>
38 <li>move up (square grid): <input id="key_square_move_up" type="text" value="w" /> (hint: ArrowUp)
39 <li>move left (square grid): <input id="key_square_move_left" type="text" value="a" /> (hint: ArrowLeft)
40 <li>move down (square grid): <input id="key_square_move_down" type="text" value="s" /> (hint: ArrowDown)
41 <li>move right (square grid): <input id="key_square_move_right" type="text" value="d" /> (hint: ArrowRight)
42 <li>move up-left (hex grid): <input id="key_hex_move_upleft" type="text" value="w" />
43 <li>move up-right (hex grid): <input id="key_hex_move_upright" type="text" value="e" />
44 <li>move right (hex grid): <input id="key_hex_move_right" type="text" value="d" />
45 <li>move down-right (hex grid): <input id="key_hex_move_downright" type="text" value="x" />
46 <li>move down-left (hex grid): <input id="key_hex_move_downleft" type="text" value="y" />
47 <li>move left (hex grid): <input id="key_hex_move_left" type="text" value="a" />
48 <li>help: <input id="key_help" type="text" value="h" />
49 <li>flatten surroundings: <input id="key_flatten" type="text" value="F" />
50 <li>teleport: <input id="key_teleport" type="text" value="p" />
51 <li>take thing under player: <input id="key_take_thing" type="text" value="z" />
52 <li>drop carried thing: <input id="key_drop_thing" type="text" value="u" />
53 <li>switch to chat mode: <input id="key_switch_to_chat" type="text" value="t" />
54 <li>switch to play mode: <input id="key_switch_to_play" type="text" value="p" />
55 <li>switch to study mode: <input id="key_switch_to_study" type="text" value="?" />
56 <li>edit tile (from play mode): <input id="key_switch_to_edit" type="text" value="m" />
57 <li>enter tile password (from play mode): <input id="key_switch_to_password" type="text" value="P" />
58 <li>enter admin password (from play mode): <input id="key_switch_to_admin" type="text" value="A" />
59 <li>change tile control password (from play mode): <input id="key_switch_to_control_pw_type" type="text" value="C" />
60 <li>annotate tile (from play mode): <input id="key_switch_to_annotate" type="text" value="M" />
61 <li>annotate portal (from play mode): <input id="key_switch_to_portal" type="text" value="T" />
62 <li>toggle terrain/annotations/control view (from study mode): <input id="key_toggle_map_mode" type="text" value="M" />
63 </ul>
64 </div>
65 <script>
66 "use strict";
67 let websocket_location = "wss://plomlompom.com/rogue_chat/";
68 //let websocket_location = "ws://localhost:8000/";
69
70 let mode_helps = {
71     'play': {
72         'short': 'play',
73         'long': 'This mode allows you to interact with the map.'
74     },
75     'study': {
76         'short': 'study',
77         'long': '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.'},
78     'edit': {
79         'short': 'terrain edit',
80         'long': '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.'
81     },
82     'control_pw_type': {
83         'short': 'change tile control password',
84         'long': 'This mode is the first of two steps to change the password for a tile control character.  First enter the tile control character for which you want to change the password!'
85     },
86     'control_pw_pw': {
87         'short': '',
88         'long': 'This mode is the second of two steps to change the password for a tile control character.  Enter the new password for the tile control character you chose.'
89     },
90     'annotate': {
91         'short': 'annotation',
92         'long': '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.'
93     },
94     'portal': {
95         'short': 'edit portal',
96         'long': '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.'
97     },
98     'chat': {
99         'short': 'chat mode',
100         'long': '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:'
101     },
102     'login': {
103         'short': '',
104         'long': 'Pick your player name.'
105     },
106     'waiting_for_server': {
107         'short': '',
108         'long': 'Waiting for a server response.'
109     },
110     'post_login_wait': {
111         'short': '',
112         'long': 'Waiting for a server response.'
113     },
114     'password': {
115         'short': 'password input',
116         'long': '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.'
117     },
118     'admin': {
119         'short': 'become admin',
120         'long': 'This mode allows you to become admin if you know an admin password.'
121     }
122 }
123
124 let rows_selector = document.getElementById("n_rows");
125 let cols_selector = document.getElementById("n_cols");
126 let key_selectors = document.querySelectorAll('[id^="key_"]');
127
128 function restore_selector_value(selector) {
129     let stored_selection = window.localStorage.getItem(selector.id);
130     if (stored_selection) {
131         selector.value = stored_selection;
132     }
133 }
134 restore_selector_value(rows_selector);
135 restore_selector_value(cols_selector);
136 for (let key_selector of key_selectors) {
137     restore_selector_value(key_selector);
138 }
139
140 let terminal = {
141   foreground: 'white',
142   background: 'black',
143   initialize: function() {
144     this.rows = rows_selector.value;
145     this.cols = cols_selector.value;
146     this.pre_el = document.getElementById("terminal");
147     this.pre_el.style.color = this.foreground;
148     this.pre_el.style.backgroundColor = this.background;
149     this.content = [];
150       let line = []
151     for (let y = 0, x = 0; y <= this.rows; x++) {
152         if (x == this.cols) {
153             x = 0;
154             y += 1;
155             this.content.push(line);
156             line = [];
157             if (y == this.rows) {
158                 break;
159             }
160         }
161         line.push(' ');
162     }
163   },
164   blink_screen: function() {
165       this.pre_el.style.color = this.background;
166       this.pre_el.style.backgroundColor = this.foreground;
167       setTimeout(() => {
168           this.pre_el.style.color = this.foreground;
169           this.pre_el.style.backgroundColor = this.background;
170       }, 100);
171   },
172   refresh: function() {
173       let pre_string = '';
174       for (let y = 0; y < this.rows; y++) {
175           let line = this.content[y].join('');
176           pre_string += line + '\n';
177       }
178       this.pre_el.textContent = pre_string;
179   },
180   write: function(start_y, start_x, msg) {
181       for (let x = start_x, i = 0; x < this.cols && i < msg.length; x++, i++) {
182           this.content[start_y][x] = msg[i];
183       }
184   },
185   drawBox: function(start_y, start_x, height, width) {
186     let end_y = start_y + height;
187     let end_x = start_x + width;
188     for (let y = start_y, x = start_x; y < this.rows; x++) {
189       if (x == end_x) {
190         x = start_x;
191         y += 1;
192         if (y == end_y) {
193             break;
194         }
195       };
196       this.content[y][x] = ' ';
197     }
198   },
199 }
200 terminal.initialize();
201
202 let parser = {
203   tokenize: function(str) {
204     let tokens = [];
205     let token = ''
206     let quoted = false;
207     let escaped = false;
208     for (let i = 0; i < str.length; i++) {
209       let c = str[i];
210       if (quoted) {
211         if (escaped) {
212           token += c;
213           escaped = false;
214         } else if (c == '\\') {
215           escaped = true;
216         } else if (c == '"') {
217           quoted = false
218         } else {
219           token += c;
220         }
221       } else if (c == '"') {
222         quoted = true
223       } else if (c === ' ') {
224         if (token.length > 0) {
225           tokens.push(token);
226           token = '';
227         }
228       } else {
229         token += c;
230       }
231     }
232     if (token.length > 0) {
233       tokens.push(token);
234     }
235     return tokens;
236   },
237   parse_yx: function(position_string) {
238     let coordinate_strings = position_string.split(',')
239     let position = [0, 0];
240     position[0] = parseInt(coordinate_strings[0].slice(2));
241     position[1] = parseInt(coordinate_strings[1].slice(2));
242     return position;
243   },
244 }
245
246 class Thing {
247     constructor(yx) {
248         this.position = yx;
249     }
250 }
251
252 let server = {
253     init: function(url) {
254         this.url = url;
255         this.websocket = new WebSocket(this.url);
256         this.websocket.onopen = function(event) {
257             server.connected = true;
258             game.thing_types = {};
259             game.terrains = {};
260             server.send(['TASKS']);
261             server.send(['TERRAINS']);
262             server.send(['THING_TYPES']);
263             tui.log_msg("@ server connected! :)");
264             tui.switch_mode('login');
265         };
266         this.websocket.onclose = function(event) {
267             server.connected = false;
268             tui.switch_mode('waiting_for_server');
269             tui.log_msg("@ server disconnected :(");
270         };
271             this.websocket.onmessage = this.handle_event;
272         },
273     reconnect_to: function(url) {
274         this.websocket.close();
275         this.init(url);
276     },
277     send: function(tokens) {
278         this.websocket.send(unparser.untokenize(tokens));
279     },
280     handle_event: function(event) {
281         let tokens = parser.tokenize(event.data);
282         if (tokens[0] === 'TURN') {
283             game.turn_complete = false;
284             explorer.empty_info_db();
285             game.things = {};
286             game.portals = {};
287             game.turn = parseInt(tokens[1]);
288         } else if (tokens[0] === 'THING') {
289             let t = game.get_thing(tokens[3], true);
290             t.position = parser.parse_yx(tokens[1]);
291             t.type_ = tokens[2];
292         } else if (tokens[0] === 'THING_NAME') {
293             let t = game.get_thing(tokens[1], false);
294             if (t) {
295                 t.name_ = tokens[2];
296             };
297         } else if (tokens[0] === 'THING_CHAR') {
298             let t = game.get_thing(tokens[1], false);
299             if (t) {
300                 t.player_char = tokens[2];
301             };
302         } else if (tokens[0] === 'TASKS') {
303             game.tasks = tokens[1].split(',');
304             tui.mode_edit.legal = game.tasks.includes('WRITE');
305         } else if (tokens[0] === 'THING_TYPE') {
306             game.thing_types[tokens[1]] = tokens[2]
307         } else if (tokens[0] === 'TERRAIN') {
308             game.terrains[tokens[1]] = tokens[2]
309         } else if (tokens[0] === 'MAP') {
310             game.map_geometry = tokens[1];
311             tui.init_keys();
312             game.map_size = parser.parse_yx(tokens[2]);
313             game.map = tokens[3]
314         } else if (tokens[0] === 'FOV') {
315             game.fov = tokens[1]
316         } else if (tokens[0] === 'MAP_CONTROL') {
317             game.map_control = tokens[1]
318         } else if (tokens[0] === 'GAME_STATE_COMPLETE') {
319             game.turn_complete = true;
320             if (tui.mode.name == 'post_login_wait') {
321                 tui.switch_mode('play');
322             } else if (tui.mode.name == 'study') {
323                 explorer.query_info();
324             }
325             tui.full_refresh();
326         } else if (tokens[0] === 'CHAT') {
327              tui.log_msg('# ' + tokens[1], 1);
328         } else if (tokens[0] === 'PLAYER_ID') {
329             game.player_id = parseInt(tokens[1]);
330         } else if (tokens[0] === 'LOGIN_OK') {
331             this.send(['GET_GAMESTATE']);
332             tui.switch_mode('post_login_wait');
333         } else if (tokens[0] === 'PORTAL') {
334             let position = parser.parse_yx(tokens[1]);
335             game.portals[position] = tokens[2];
336         } else if (tokens[0] === 'ANNOTATION_HINT') {
337             let position = parser.parse_yx(tokens[1]);
338             explorer.info_hints = explorer.info_hints.concat([position]);
339         } else if (tokens[0] === 'ANNOTATION') {
340             let position = parser.parse_yx(tokens[1]);
341             explorer.update_info_db(position, tokens[2]);
342             tui.restore_input_values();
343             tui.full_refresh();
344         } else if (tokens[0] === 'UNHANDLED_INPUT') {
345             tui.log_msg('? unknown command');
346         } else if (tokens[0] === 'PLAY_ERROR') {
347             tui.log_msg('? ' + tokens[1]);
348             terminal.blink_screen();
349         } else if (tokens[0] === 'ARGUMENT_ERROR') {
350             tui.log_msg('? syntax error: ' + tokens[1]);
351         } else if (tokens[0] === 'GAME_ERROR') {
352             tui.log_msg('? game error: ' + tokens[1]);
353         } else if (tokens[0] === 'PONG') {
354             ;
355         } else {
356             tui.log_msg('? unhandled input: ' + event.data);
357         }
358     }
359 }
360
361 let unparser = {
362     quote: function(str) {
363         let quoted = ['"'];
364         for (let i = 0; i < str.length; i++) {
365             let c = str[i];
366             if (['"', '\\'].includes(c)) {
367                 quoted.push('\\');
368             };
369             quoted.push(c);
370         }
371         quoted.push('"');
372         return quoted.join('');
373     },
374     to_yx: function(yx_coordinate) {
375         return "Y:" + yx_coordinate[0] + ",X:" + yx_coordinate[1];
376     },
377     untokenize: function(tokens) {
378         let quoted_tokens = [];
379         for (let token of tokens) {
380             quoted_tokens.push(this.quote(token));
381         }
382         return quoted_tokens.join(" ");
383     }
384 }
385
386 class Mode {
387     constructor(name, has_input_prompt=false, shows_info=false,
388                 is_intro=false, is_single_char_entry=false) {
389         this.name = name;
390         this.short_desc = mode_helps[name].short;
391         this.available_modes = [];
392         this.has_input_prompt = has_input_prompt;
393         this.shows_info= shows_info;
394         this.is_intro = is_intro;
395         this.help_intro = mode_helps[name].long;
396         this.is_single_char_entry = is_single_char_entry;
397         this.legal = true;
398     }
399     *iter_available_modes() {
400         for (let mode_name of this.available_modes) {
401             let mode = tui['mode_' + mode_name];
402             if (!mode.legal) {
403                 continue;
404             }
405             let key = tui.keys['switch_to_' + mode.name];
406             yield [mode, key]
407         }
408     }
409     list_available_modes() {
410         let msg = ''
411         if (this.available_modes.length > 0) {
412             msg += 'Other modes available from here:\n';
413             for (let [mode, key] of this.iter_available_modes()) {
414                 msg += '[' + key + '] – ' + mode.short_desc + '\n';
415             }
416         }
417         return msg;
418     }
419     mode_switch_on_key(key_event) {
420         for (let [mode, key] of this.iter_available_modes()) {
421             if (key_event.key == key) {
422                 event.preventDefault();
423                 tui.switch_mode(mode.name);
424                 return true;
425             };
426         }
427         return false;
428     }
429 }
430 let tui = {
431   log: [],
432   input_prompt: '> ',
433   input_lines: [],
434   window_width: terminal.cols / 2,
435   height_turn_line: 1,
436   height_mode_line: 1,
437   height_input: 1,
438   password: 'foo',
439   show_help: false,
440   mode_waiting_for_server: new Mode('waiting_for_server',
441                                      false, false, true),
442   mode_login: new Mode('login', true, false, true),
443   mode_post_login_wait: new Mode('post_login_wait'),
444   mode_chat: new Mode('chat', true),
445   mode_annotate: new Mode('annotate', true, true),
446   mode_play: new Mode('play'),
447   mode_study: new Mode('study', false, true),
448   mode_edit: new Mode('edit', false, false, false, true),
449   mode_control_pw_type: new Mode('control_pw_type',
450                                   false, false, false, true),
451   mode_portal: new Mode('portal', true, true),
452   mode_password: new Mode('password', true),
453   mode_admin: new Mode('admin', true),
454   mode_control_pw_pw: new Mode('control_pw_pw', true),
455   init: function() {
456       this.mode_play.available_modes = ["chat", "study", "edit",
457                                         "annotate", "portal",
458                                         "password", "admin",
459                                         "control_pw_type"]
460       this.mode_study.available_modes = ["chat", "play"]
461       this.mode = this.mode_waiting_for_server;
462       this.inputEl = document.getElementById("input");
463       this.inputEl.focus();
464       this.recalc_input_lines();
465       this.height_header = this.height_turn_line + this.height_mode_line;
466       this.log_msg("@ waiting for server connection ...");
467       this.init_keys();
468   },
469   init_keys: function() {
470     this.keys = {};
471     for (let key_selector of key_selectors) {
472         this.keys[key_selector.id.slice(4)] = key_selector.value;
473     }
474     this.movement_keys = {
475         [this.keys.square_move_up]: 'UP',
476         [this.keys.square_move_left]: 'LEFT',
477         [this.keys.square_move_down]: 'DOWN',
478         [this.keys.square_move_right]: 'RIGHT'
479     };
480     if (game.map_geometry == 'Hex') {
481         this.movement_keys = {
482             [this.keys.hex_move_upleft]: 'UPLEFT',
483             [this.keys.hex_move_upright]: 'UPRIGHT',
484             [this.keys.hex_move_right]: 'RIGHT',
485             [this.keys.hex_move_downright]: 'DOWNRIGHT',
486             [this.keys.hex_move_downleft]: 'DOWNLEFT',
487             [this.keys.hex_move_left]: 'LEFT'
488         };
489     };
490   },
491   switch_mode: function(mode_name) {
492     this.inputEl.focus();
493     this.map_mode = 'terrain';
494     this.mode = this['mode_' + mode_name];
495     if (this.mode.shows_info && game.player_id in game.things) {
496       explorer.position = game.things[game.player_id].position;
497       explorer.query_info();
498     }
499     this.empty_input();
500     this.restore_input_values();
501     document.getElementById("take_thing").disabled = true;
502     document.getElementById("drop_thing").disabled = true;
503     document.getElementById("flatten").disabled = true;
504     document.getElementById("teleport").disabled = true;
505     document.getElementById("toggle_map_mode").disabled = true;
506     document.getElementById("switch_to_chat").disabled = true;
507     document.getElementById("switch_to_play").disabled = true;
508     document.getElementById("switch_to_study").disabled = true;
509     document.getElementById("switch_to_edit").disabled = true;
510     document.getElementById("switch_to_portal").disabled = true;
511     document.getElementById("switch_to_annotate").disabled = true;
512     document.getElementById("switch_to_password").disabled = true;
513     document.getElementById("switch_to_admin").disabled = true;
514     document.getElementById("switch_to_control_pw_type").disabled = true;
515     document.getElementById("move_left").disabled = true;
516     document.getElementById("move_upleft").disabled = true;
517     document.getElementById("move_up").disabled = true;
518     document.getElementById("move_upright").disabled = true;
519     document.getElementById("move_downleft").disabled = true;
520     document.getElementById("move_down").disabled = true;
521     document.getElementById("move_downright").disabled = true;
522     document.getElementById("move_right").disabled = true;
523     if (this.mode.name == 'play' || this.mode.name == 'study') {
524         document.getElementById("move_left").disabled = false;
525         document.getElementById("move_right").disabled = false;
526         if (game.map_geometry == 'Hex') {
527             document.getElementById("move_upleft").disabled = false;
528             document.getElementById("move_upright").disabled = false;
529             document.getElementById("move_downleft").disabled = false;
530             document.getElementById("move_downright").disabled = false;
531         } else {
532             document.getElementById("move_up").disabled = false;
533             document.getElementById("move_down").disabled = false;
534         }
535     }
536     if (!this.mode.is_intro && this.mode.name != 'play') {
537         document.getElementById("switch_to_play").disabled = false;
538     }
539     if (!this.mode.is_intro && this.mode.name != 'study') {
540         document.getElementById("switch_to_study").disabled = false;
541     }
542     if (!this.mode.is_intro && this.mode.name != 'chat') {
543         document.getElementById("switch_to_chat").disabled = false;
544     }
545     if (this.mode.name == 'login') {
546         if (this.login_name) {
547             server.send(['LOGIN', this.login_name]);
548         } else {
549             this.log_msg("? need login name");
550         }
551     } else if (this.mode.name == 'play') {
552         if (game.tasks.includes('PICK_UP')) {
553             document.getElementById("take_thing").disabled = false;
554         }
555         if (game.tasks.includes('DROP')) {
556             document.getElementById("drop_thing").disabled = false;
557         }
558         if (game.tasks.includes('FLATTEN_SURROUNDINGS')) {
559             document.getElementById("flatten").disabled = false;
560         }
561         if (game.tasks.includes('MOVE')) {
562         }
563         document.getElementById("teleport").disabled = false;
564         document.getElementById("switch_to_annotate").disabled = false;
565         document.getElementById("switch_to_edit").disabled = false;
566         document.getElementById("switch_to_portal").disabled = false;
567         document.getElementById("switch_to_password").disabled = false;
568         document.getElementById("switch_to_admin").disabled = false;
569         document.getElementById("switch_to_control_pw_type").disabled = false;
570     } else if (this.mode.name == 'study') {
571         document.getElementById("toggle_map_mode").disabled = false;
572     } else if (this.mode.is_single_char_entry) {
573         this.show_help = true;
574     } else if (this.mode.name == 'admin') {
575         this.log_msg('@ enter admin password:')
576     } else if (this.mode.name == 'control_pw_pw') {
577         this.log_msg('@ enter tile control password for "' + this.tile_control_char + '":');
578     }
579     this.full_refresh();
580   },
581   restore_input_values: function() {
582       if (this.mode.name == 'annotate' && explorer.position in explorer.info_db) {
583           let info = explorer.info_db[explorer.position];
584           if (info != "(none)") {
585               this.inputEl.value = info;
586               this.recalc_input_lines();
587           }
588       } else if (this.mode.name == 'portal' && explorer.position in game.portals) {
589           let portal = game.portals[explorer.position]
590           this.inputEl.value = portal;
591           this.recalc_input_lines();
592       } else if (this.mode.name == 'password') {
593           this.inputEl.value = this.password;
594           this.recalc_input_lines();
595       }
596   },
597   empty_input: function(str) {
598       this.inputEl.value = "";
599       if (this.mode.has_input_prompt) {
600           this.recalc_input_lines();
601       } else {
602           this.height_input = 0;
603       }
604   },
605   recalc_input_lines: function() {
606       this.input_lines = this.msg_into_lines_of_width(this.input_prompt + this.inputEl.value, this.window_width);
607       this.height_input = this.input_lines.length;
608   },
609   msg_into_lines_of_width: function(msg, width) {
610     let chunk = "";
611     let lines = [];
612     for (let i = 0, x = 0; i < msg.length; i++, x++) {
613       if (x >= width || msg[i] == "\n") {
614         lines.push(chunk);
615         chunk = "";
616         x = 0;
617       };
618       if (msg[i] != "\n") {
619         chunk += msg[i];
620       }
621     }
622     lines.push(chunk);
623     return lines;
624   },
625   log_msg: function(msg) {
626       this.log.push(msg);
627       while (this.log.length > 100) {
628         this.log.shift();
629       };
630       this.full_refresh();
631   },
632   draw_map: function() {
633     let map_lines_split = [];
634     let line = [];
635     let map_content = game.map;
636     if (this.map_mode == 'control') {
637         map_content = game.map_control;
638     }
639     for (let i = 0, j = 0; i < game.map.length; i++, j++) {
640         if (j == game.map_size[1]) {
641             map_lines_split.push(line);
642             line = [];
643             j = 0;
644         };
645         line.push(map_content[i] + ' ');
646     };
647     map_lines_split.push(line);
648     if (this.map_mode == 'annotations') {
649         for (const coordinate of explorer.info_hints) {
650             map_lines_split[coordinate[0]][coordinate[1]] = 'A ';
651         }
652     } else if (this.map_mode == 'terrain') {
653         for (const p in game.portals) {
654             let coordinate = p.split(',')
655             map_lines_split[coordinate[0]][coordinate[1]] = 'P ';
656         }
657         let used_positions = [];
658         for (const thing_id in game.things) {
659             let t = game.things[thing_id];
660             let symbol = game.thing_types[t.type_];
661             let meta_char = ' ';
662             if (t.player_char) {
663                 meta_char = t.player_char;
664             }
665             if (used_positions.includes(t.position.toString())) {
666                 meta_char = '+';
667             };
668             map_lines_split[t.position[0]][t.position[1]] = symbol + meta_char;
669             used_positions.push(t.position.toString());
670         };
671     }
672     if (tui.mode.shows_info) {
673         map_lines_split[explorer.position[0]][explorer.position[1]] = '??';
674     }
675     let map_lines = []
676     if (game.map_geometry == 'Square') {
677         for (let line_split of map_lines_split) {
678             map_lines.push(line_split.join(''));
679         };
680     } else if (game.map_geometry == 'Hex') {
681         let indent = 0
682         for (let line_split of map_lines_split) {
683             map_lines.push(' '.repeat(indent) + line_split.join(''));
684             if (indent == 0) {
685                 indent = 1;
686             } else {
687                 indent = 0;
688             };
689         };
690     }
691     let window_center = [terminal.rows / 2, this.window_width / 2];
692     let player = game.things[game.player_id];
693     let center_position = [player.position[0], player.position[1]];
694     if (tui.mode.shows_info) {
695         center_position = [explorer.position[0], explorer.position[1]];
696     }
697     center_position[1] = center_position[1] * 2;
698     let offset = [center_position[0] - window_center[0],
699                   center_position[1] - window_center[1]]
700     if (game.map_geometry == 'Hex' && offset[0] % 2) {
701         offset[1] += 1;
702     };
703     let term_y = Math.max(0, -offset[0]);
704     let term_x = Math.max(0, -offset[1]);
705     let map_y = Math.max(0, offset[0]);
706     let map_x = Math.max(0, offset[1]);
707     for (; term_y < terminal.rows && map_y < game.map_size[0]; term_y++, map_y++) {
708         let to_draw = map_lines[map_y].slice(map_x, this.window_width + offset[1]);
709         terminal.write(term_y, term_x, to_draw);
710     }
711   },
712   draw_mode_line: function() {
713       let help = 'hit [' + this.keys.help + '] for help';
714       if (this.mode.has_input_prompt) {
715           help = 'enter /help for help';
716       }
717       terminal.write(0, this.window_width, 'MODE: ' + this.mode.name + ' – ' + help);
718   },
719   draw_turn_line: function(n) {
720     terminal.write(1, this.window_width, 'TURN: ' + game.turn);
721   },
722   draw_history: function() {
723       let log_display_lines = [];
724       for (let line of this.log) {
725           log_display_lines = log_display_lines.concat(this.msg_into_lines_of_width(line, this.window_width));
726       };
727       for (let y = terminal.rows - 1 - this.height_input,
728                i = log_display_lines.length - 1;
729            y >= this.height_header && i >= 0;
730            y--, i--) {
731           terminal.write(y, this.window_width, log_display_lines[i]);
732       }
733   },
734   draw_info: function() {
735     let lines = this.msg_into_lines_of_width(explorer.get_info(), this.window_width);
736     for (let y = this.height_header, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
737       terminal.write(y, this.window_width, lines[i]);
738     }
739   },
740   draw_input: function() {
741     if (this.mode.has_input_prompt) {
742         for (let y = terminal.rows - this.height_input, i = 0; i < this.input_lines.length; y++, i++) {
743             terminal.write(y, this.window_width, this.input_lines[i]);
744         }
745     }
746   },
747   draw_help: function() {
748       let movement_keys_desc = Object.keys(this.movement_keys).join(',');
749       let content = this.mode.name + " mode help\n\n" + this.mode.help_intro + "\n\n";
750       if (this.mode.name == 'play') {
751           content += "Available actions:\n";
752           if (game.tasks.includes('MOVE')) {
753               content += "[" + movement_keys_desc + "] – move player\n";
754           }
755           if (game.tasks.includes('PICK_UP')) {
756               content += "[" + this.keys.take_thing + "] – take thing under player\n";
757           }
758           if (game.tasks.includes('DROP')) {
759               content += "[" + this.keys.drop_thing + "] – drop carried thing\n";
760           }
761           if (game.tasks.includes('FLATTEN_SURROUNDINGS')) {
762               content += "[" + tui.keys.flatten + "] – flatten player's surroundings\n";
763           }
764           content += "[" + tui.keys.teleport + "] – teleport to other space\n";
765           content += '\n';
766       } else if (this.mode.name == 'study') {
767           content += "Available actions:\n";
768           content += '[' + movement_keys_desc + '] – move question mark\n';
769           content += '[' + this.keys.toggle_map_mode + '] – toggle view between terrain, annotations, and password protection areas\n';
770           content += '\n';
771       } else if (this.mode.name == 'chat') {
772           content += '/nick NAME – re-name yourself to NAME\n';
773           content += '/' + this.keys.switch_to_play + ' or /play – switch to play mode\n';
774           content += '/' + this.keys.switch_to_study + ' or /study – switch to study mode\n';
775       }
776       content += this.mode.list_available_modes();
777       let start_x = 0;
778       if (!this.mode.has_input_prompt) {
779           start_x = this.window_width
780       }
781       terminal.drawBox(0, start_x, terminal.rows, this.window_width);
782       let lines = this.msg_into_lines_of_width(content, this.window_width);
783       for (let y = 0, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
784           terminal.write(y, start_x, lines[i]);
785       }
786   },
787   full_refresh: function() {
788     terminal.drawBox(0, 0, terminal.rows, terminal.cols);
789     if (this.mode.is_intro) {
790         this.draw_history();
791         this.draw_input();
792     } else {
793         if (game.turn_complete) {
794             this.draw_map();
795             this.draw_turn_line();
796         }
797         this.draw_mode_line();
798         if (this.mode.shows_info) {
799           this.draw_info();
800         } else {
801           this.draw_history();
802         }
803         this.draw_input();
804     }
805     if (this.show_help) {
806         this.draw_help();
807     }
808     terminal.refresh();
809   }
810 }
811
812 let game = {
813     init: function() {
814         this.things = {};
815         this.turn = -1;
816         this.map = "";
817         this.map_control = "";
818         this.map_size = [0,0];
819         this.player_id = -1;
820         this.portals = {};
821         this.tasks = {};
822     },
823     get_thing: function(id_, create_if_not_found=false) {
824         if (id_ in game.things) {
825             return game.things[id_];
826         } else if (create_if_not_found) {
827             let t = new Thing([0,0]);
828             game.things[id_] = t;
829             return t;
830         };
831     },
832     move: function(start_position, direction) {
833         let target = [start_position[0], start_position[1]];
834         if (direction == 'LEFT') {
835             target[1] -= 1;
836         } else if (direction == 'RIGHT') {
837             target[1] += 1;
838         } else if (game.map_geometry == 'Square') {
839             if (direction == 'UP') {
840                 target[0] -= 1;
841             } else if (direction == 'DOWN') {
842                 target[0] += 1;
843             };
844         } else if (game.map_geometry == 'Hex') {
845             let start_indented = start_position[0] % 2;
846             if (direction == 'UPLEFT') {
847                 target[0] -= 1;
848                 if (!start_indented) {
849                     target[1] -= 1;
850                 }
851             } else if (direction == 'UPRIGHT') {
852                 target[0] -= 1;
853                 if (start_indented) {
854                     target[1] += 1;
855                 }
856             } else if (direction == 'DOWNLEFT') {
857                 target[0] += 1;
858                 if (!start_indented) {
859                     target[1] -= 1;
860                 }
861             } else if (direction == 'DOWNRIGHT') {
862                 target[0] += 1;
863                 if (start_indented) {
864                     target[1] += 1;
865                 }
866             };
867         };
868         if (target[0] < 0 || target[1] < 0 ||
869             target[0] >= this.map_size[0] || target[1] >= this.map_size[1]) {
870             return null;
871         };
872         return target;
873     },
874     teleport: function() {
875         let player = this.get_thing(game.player_id);
876         if (player.position in this.portals) {
877             server.reconnect_to(this.portals[player.position]);
878         } else {
879             terminal.blink_screen();
880             tui.log_msg('? not standing on portal')
881         }
882     }
883 }
884
885 game.init();
886 tui.init();
887 tui.full_refresh();
888 server.init(websocket_location);
889
890 let explorer = {
891     position: [0,0],
892     info_db: {},
893     info_hints: [],
894     move: function(direction) {
895         let target = game.move(this.position, direction);
896         if (target) {
897             this.position = target
898             this.query_info();
899         } else {
900             terminal.blink_screen();
901         };
902     },
903     update_info_db: function(yx, str) {
904         this.info_db[yx] = str;
905         if (tui.mode.name == 'study') {
906             tui.full_refresh();
907         }
908     },
909     empty_info_db: function() {
910         this.info_db = {};
911         this.info_hints = [];
912         if (tui.mode.name == 'study') {
913             tui.full_refresh();
914         }
915     },
916     query_info: function() {
917         server.send(["GET_ANNOTATION", unparser.to_yx(explorer.position)]);
918     },
919     get_info: function() {
920         let position_i = this.position[0] * game.map_size[1] + this.position[1];
921         if (game.fov[position_i] != '.') {
922             return 'outside field of view';
923         };
924         let info = "";
925         let terrain_char = game.map[position_i]
926         let terrain_desc = '?'
927         if (game.terrains[terrain_char]) {
928             terrain_desc = game.terrains[terrain_char];
929         };
930         info += 'TERRAIN: "' + terrain_char + '" / ' + terrain_desc + "\n";
931         let protection = game.map_control[position_i];
932         if (protection == '.') {
933             protection = 'unprotected';
934         };
935         info += 'PROTECTION: ' + protection + '\n';
936         for (let t_id in game.things) {
937              let t = game.things[t_id];
938              if (t.position[0] == this.position[0] && t.position[1] == this.position[1]) {
939                  let symbol = game.thing_types[t.type_];
940                  info += "THING: " + t.type_ + " / " + symbol;
941                  if (t.player_char) {
942                      info += t.player_char;
943                  };
944                  if (t.name_) {
945                      info += " (" + t.name_ + ")";
946                  }
947                  info += "\n";
948              }
949         }
950         if (this.position in game.portals) {
951             info += "PORTAL: " + game.portals[this.position] + "\n";
952         }
953         if (this.position in this.info_db) {
954             info += "ANNOTATIONS: " + this.info_db[this.position];
955         } else {
956             info += 'waiting …';
957         }
958         return info;
959     },
960     annotate: function(msg) {
961         if (msg.length == 0) {
962             msg = " ";  // triggers annotation deletion
963         }
964         server.send(["ANNOTATE", unparser.to_yx(explorer.position), msg, tui.password]);
965     },
966     set_portal: function(msg) {
967         if (msg.length == 0) {
968             msg = " ";  // triggers portal deletion
969         }
970         server.send(["PORTAL", unparser.to_yx(explorer.position), msg, tui.password]);
971     }
972 }
973
974 tui.inputEl.addEventListener('input', (event) => {
975     if (tui.mode.has_input_prompt) {
976         let max_length = tui.window_width * terminal.rows - tui.input_prompt.length;
977         if (tui.inputEl.value.length > max_length) {
978             tui.inputEl.value = tui.inputEl.value.slice(0, max_length);
979         };
980         tui.recalc_input_lines();
981     } else if (tui.mode.name == 'edit' && tui.inputEl.value.length > 0) {
982         server.send(["TASK:WRITE", tui.inputEl.value[0], tui.password]);
983         tui.switch_mode('play');
984     } else if (tui.mode.name == 'control_pw_type' && tui.inputEl.value.length > 0) {
985         tui.tile_control_char = tui.inputEl.value[0];
986         tui.switch_mode('control_pw_pw');
987     }
988     tui.full_refresh();
989 }, false);
990 tui.inputEl.addEventListener('keydown', (event) => {
991     tui.show_help = false;
992     if (event.key == 'Enter') {
993         event.preventDefault();
994     }
995     if (tui.mode.has_input_prompt && event.key == 'Enter' && tui.inputEl.value == '/help') {
996         tui.show_help = true;
997         tui.empty_input();
998         tui.restore_input_values();
999     } else if (!tui.mode.has_input_prompt && event.key == tui.keys.help
1000                && !tui.mode.is_single_char_entry) {
1001         tui.show_help = true;
1002     } else if (tui.mode.name == 'login' && event.key == 'Enter') {
1003         tui.login_name = tui.inputEl.value;
1004         server.send(['LOGIN', tui.inputEl.value]);
1005         tui.empty_input();
1006     } else if (tui.mode.name == 'control_pw_pw' && event.key == 'Enter') {
1007         if (tui.inputEl.value.length == 0) {
1008             tui.log_msg('@ aborted');
1009         } else {
1010             server.send(['SET_MAP_CONTROL_PASSWORD',
1011                         tui.tile_control_char, tui.inputEl.value]);
1012         }
1013         tui.switch_mode('play');
1014     } else if (tui.mode.name == 'portal' && event.key == 'Enter') {
1015         explorer.set_portal(tui.inputEl.value);
1016         tui.switch_mode('play');
1017     } else if (tui.mode.name == 'annotate' && event.key == 'Enter') {
1018         explorer.annotate(tui.inputEl.value);
1019         tui.switch_mode('play');
1020     } else if (tui.mode.name == 'password' && event.key == 'Enter') {
1021         if (tui.inputEl.value.length == 0) {
1022             tui.inputEl.value = " ";
1023         }
1024         tui.password = tui.inputEl.value
1025         tui.switch_mode('play');
1026     } else if (tui.mode.name == 'admin' && event.key == 'Enter') {
1027         server.send(['BECOME_ADMIN', tui.inputEl.value]);
1028         tui.switch_mode('play');
1029     } else if (tui.mode.name == 'chat' && event.key == 'Enter') {
1030         let tokens = parser.tokenize(tui.inputEl.value);
1031         if (tokens.length > 0 && tokens[0].length > 0) {
1032             if (tui.inputEl.value[0][0] == '/') {
1033                 if (tokens[0].slice(1) == 'play' || tokens[0][1] == tui.keys.switch_to_play) {
1034                     tui.switch_mode('play');
1035                 } else if (tokens[0].slice(1) == 'study' || tokens[0][1] == tui.keys.switch_to_study) {
1036                     tui.switch_mode('study');
1037                 } else if (tokens[0].slice(1) == 'nick') {
1038                     if (tokens.length > 1) {
1039                         server.send(['NICK', tokens[1]]);
1040                     } else {
1041                         tui.log_msg('? need new name');
1042                     }
1043                 } else {
1044                     tui.log_msg('? unknown command');
1045                 }
1046             } else {
1047                     server.send(['ALL', tui.inputEl.value]);
1048             }
1049         } else if (tui.inputEl.valuelength > 0) {
1050                 server.send(['ALL', tui.inputEl.value]);
1051         }
1052         tui.empty_input();
1053     } else if (tui.mode.name == 'play') {
1054           if (tui.mode.mode_switch_on_key(event)) {
1055               null;
1056           } else if (event.key === tui.keys.flatten
1057                      && game.tasks.includes('FLATTEN_SURROUNDINGS')) {
1058               server.send(["TASK:FLATTEN_SURROUNDINGS", tui.password]);
1059           } else if (event.key === tui.keys.take_thing
1060                      && game.tasks.includes('PICK_UP')) {
1061               server.send(["TASK:PICK_UP"]);
1062           } else if (event.key === tui.keys.drop_thing
1063                      && game.tasks.includes('DROP')) {
1064               server.send(["TASK:DROP"]);
1065           } else if (event.key in tui.movement_keys
1066                      && game.tasks.includes('MOVE')) {
1067               server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1068           } else if (event.key === tui.keys.teleport) {
1069               game.teleport();
1070           } else if (event.key === tui.keys.switch_to_portal) {
1071               event.preventDefault();
1072               tui.switch_mode('portal');
1073           } else if (event.key === tui.keys.switch_to_annotate) {
1074               event.preventDefault();
1075               tui.switch_mode('annotate');
1076           };
1077     } else if (tui.mode.name == 'study') {
1078         if (tui.mode.mode_switch_on_key(event)) {
1079               null;
1080         } else if (event.key == tui.keys.switch_to_play) {
1081             tui.switch_mode('play');
1082         } else if (event.key in tui.movement_keys) {
1083             explorer.move(tui.movement_keys[event.key]);
1084         } else if (event.key == tui.keys.toggle_map_mode) {
1085             if (tui.map_mode == 'terrain') {
1086                 tui.map_mode = 'annotations';
1087             } else if (tui.map_mode == 'annotations') {
1088                 tui.map_mode = 'control';
1089             } else {
1090                 tui.map_mode = 'terrain';
1091             }
1092         };
1093     }
1094     tui.full_refresh();
1095 }, false);
1096
1097 rows_selector.addEventListener('input', function() {
1098     if (rows_selector.value % 4 != 0 || rows_selector.value < 24) {
1099         return;
1100     }
1101     window.localStorage.setItem(rows_selector.id, rows_selector.value);
1102     terminal.initialize();
1103     tui.full_refresh();
1104 }, false);
1105 cols_selector.addEventListener('input', function() {
1106     if (cols_selector.value % 4 != 0 || cols_selector.value < 80) {
1107         return;
1108     }
1109     window.localStorage.setItem(cols_selector.id, cols_selector.value);
1110     terminal.initialize();
1111     tui.window_width = terminal.cols / 2,
1112     tui.full_refresh();
1113 }, false);
1114 for (let key_selector of key_selectors) {
1115     key_selector.addEventListener('input', function() {
1116         window.localStorage.setItem(key_selector.id, key_selector.value);
1117         tui.init_keys();
1118     }, false);
1119 }
1120 window.setInterval(function() {
1121     if (server.connected) {
1122         server.send(['PING']);
1123     } else {
1124         server.reconnect_to(server.url);
1125         tui.log_msg('@ attempting reconnect …')
1126     }
1127 }, 5000);
1128 document.getElementById("terminal").onclick = function() {
1129     tui.inputEl.focus();
1130 };
1131 document.getElementById("help").onclick = function() {
1132     tui.show_help = true;
1133     tui.full_refresh();
1134 };
1135 document.getElementById("switch_to_play").onclick = function() {
1136     tui.switch_mode('play');
1137     tui.full_refresh();
1138 };
1139 document.getElementById("switch_to_study").onclick = function() {
1140     tui.switch_mode('study');
1141     tui.full_refresh();
1142 };
1143 document.getElementById("switch_to_chat").onclick = function() {
1144     tui.switch_mode('chat');
1145     tui.full_refresh();
1146 };
1147 document.getElementById("switch_to_password").onclick = function() {
1148     tui.switch_mode('password');
1149     tui.full_refresh();
1150 };
1151 document.getElementById("switch_to_edit").onclick = function() {
1152     tui.switch_mode('edit');
1153     tui.full_refresh();
1154 };
1155 document.getElementById("switch_to_annotate").onclick = function() {
1156     tui.switch_mode('annotate');
1157     tui.full_refresh();
1158 };
1159 document.getElementById("switch_to_portal").onclick = function() {
1160     tui.switch_mode('portal');
1161     tui.full_refresh();
1162 };
1163 document.getElementById("switch_to_admin").onclick = function() {
1164     tui.switch_mode('admin');
1165     tui.full_refresh();
1166 };
1167 document.getElementById("switch_to_control_pw_type").onclick = function() {
1168     tui.switch_mode('control_pw_type');
1169     tui.full_refresh();
1170 };
1171 document.getElementById("toggle_map_mode").onclick = function() {
1172     if (tui.map_mode == 'terrain') {
1173         tui.map_mode = 'annotations';
1174     } else if (tui.map_mode == 'annotations') {
1175         tui.map_mode = 'control';
1176     } else {
1177         tui.map_mode = 'terrain';
1178     }
1179     tui.full_refresh();
1180 };
1181 document.getElementById("take_thing").onclick = function() {
1182         server.send(['TASK:PICK_UP']);
1183 };
1184 document.getElementById("drop_thing").onclick = function() {
1185         server.send(['TASK:DROP']);
1186 };
1187 document.getElementById("flatten").onclick = function() {
1188     server.send(['TASK:FLATTEN_SURROUNDINGS', tui.password]);
1189 };
1190 document.getElementById("teleport").onclick = function() {
1191     game.teleport();
1192 };
1193 document.getElementById("move_upleft").onclick = function() {
1194     if (tui.mode.name == 'play') {
1195         server.send(['TASK:MOVE', 'UPLEFT']);
1196     } else {
1197         explorer.move('UPLEFT');
1198     };
1199 };
1200 document.getElementById("move_left").onclick = function() {
1201     if (tui.mode.name == 'play') {
1202         server.send(['TASK:MOVE', 'LEFT']);
1203     } else {
1204         explorer.move('LEFT');
1205     };
1206 };
1207 document.getElementById("move_downleft").onclick = function() {
1208     if (tui.mode.name == 'play') {
1209         server.send(['TASK:MOVE', 'DOWNLEFT']);
1210     } else {
1211         explorer.move('DOWNLEFT');
1212     };
1213 };
1214 document.getElementById("move_down").onclick = function() {
1215     if (tui.mode.name == 'play') {
1216         server.send(['TASK:MOVE', 'DOWN']);
1217     } else {
1218         explorer.move('DOWN');
1219     };
1220 };
1221 document.getElementById("move_up").onclick = function() {
1222     if (tui.mode.name == 'play') {
1223         server.send(['TASK:MOVE', 'UP']);
1224     } else {
1225         explorer.move('UP');
1226     };
1227 };
1228 document.getElementById("move_upright").onclick = function() {
1229     if (tui.mode.name == 'play') {
1230         server.send(['TASK:MOVE', 'UPRIGHT']);
1231     } else {
1232         explorer.move('UPRIGHT');
1233     };
1234 };
1235 document.getElementById("move_right").onclick = function() {
1236     if (tui.mode.name == 'play') {
1237         server.send(['TASK:MOVE', 'RIGHT']);
1238     } else {
1239         explorer.move('RIGHT');
1240     };
1241 };
1242 document.getElementById("move_downright").onclick = function() {
1243     if (tui.mode.name == 'play') {
1244         server.send(['TASK:MOVE', 'DOWNRIGHT']);
1245     } else {
1246         explorer.move('DOWNRIGHT');
1247     };
1248 };
1249 </script>
1250 </body></html>