13 terminal rows: <input id="n_rows" type="number" step=4 min=24 value=24 />
14 / terminal columns: <input id="n_cols" type="number" step=4 min=80 value=80 />
15 / <a href="https://plomlompom.com/repos/?p=plomrogue2;a=summary">source code</a> (includes proper terminal/ncurses client)
17 <pre id="terminal"></pre>
18 <textarea id="input" style="opacity: 0; width: 0px;"></textarea>
20 keyboard input/control: <span id="keyboard_control"></span>
22 <h3>button controls for mouse players</h3>
23 <table style="float: left">
25 <td style="text-align: right"><button id="move_upleft">up-left</button></td>
26 <td style="text-align: center"><button id="move_up">up</button></td>
27 <td><button id="move_upright">up-right</button></td>
30 <td style="text-align: right;"><button id="move_left">left</button></td>
31 <td stlye="text-align: center;">move</td>
32 <td><button id="move_right">right</button></td>
35 <td><button id="move_downleft">down-left</button></td>
36 <td style="text-align: center"><button id="move_down">down</button></td>
37 <td><button id="move_downright">down-right</button></td>
42 <td><button id="help">help</button></td>
45 <td><button id="switch_to_chat">chat mode</button><br /></td>
48 <td><button id="switch_to_study">study mode</button></td>
49 <td><button id="toggle_map_mode">toggle map view</button>
52 <td><button id="switch_to_play">play mode</button></td>
54 <button id="take_thing">pick up thing</button>
55 <button id="drop_thing">drop thing</button>
56 <button id="teleport">teleport</button>
60 <td><button id="switch_to_edit">map edit mode</button></td>
62 <button id="switch_to_write">change terrain</button>
63 <button id="flatten">flatten surroundings</button>
64 <button id="switch_to_annotate">annotate tile</button>
65 <button id="switch_to_portal">edit portal</button>
66 <button id="switch_to_name_thing">name thing</button>
67 <button id="switch_to_password">enter map edit password</button>
71 <td><button id="switch_to_admin_enter">admin mode</button></td>
73 <button id="switch_to_control_pw_type">change protection character password</button>
74 <button id="switch_to_control_tile_type">change protection areas</button>
75 <button id="toggle_tile_draw">toggle protection character drawing</button>
80 <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 />
82 <li>move up (square grid): <input id="key_square_move_up" type="text" value="w" /> (hint: ArrowUp)
83 <li>move left (square grid): <input id="key_square_move_left" type="text" value="a" /> (hint: ArrowLeft)
84 <li>move down (square grid): <input id="key_square_move_down" type="text" value="s" /> (hint: ArrowDown)
85 <li>move right (square grid): <input id="key_square_move_right" type="text" value="d" /> (hint: ArrowRight)
86 <li>move up-left (hex grid): <input id="key_hex_move_upleft" type="text" value="w" />
87 <li>move up-right (hex grid): <input id="key_hex_move_upright" type="text" value="e" />
88 <li>move right (hex grid): <input id="key_hex_move_right" type="text" value="d" />
89 <li>move down-right (hex grid): <input id="key_hex_move_downright" type="text" value="x" />
90 <li>move down-left (hex grid): <input id="key_hex_move_downleft" type="text" value="y" />
91 <li>move left (hex grid): <input id="key_hex_move_left" type="text" value="a" />
92 <li>help: <input id="key_help" type="text" value="h" />
93 <li>flatten surroundings: <input id="key_flatten" type="text" value="F" />
94 <li>teleport: <input id="key_teleport" type="text" value="p" />
95 <li>pick up thing: <input id="key_take_thing" type="text" value="z" />
96 <li>drop thing: <input id="key_drop_thing" type="text" value="u" />
97 <li><input id="key_switch_to_chat" type="text" value="t" />
98 <li><input id="key_switch_to_play" type="text" value="p" />
99 <li><input id="key_switch_to_study" type="text" value="?" />
100 <li><input id="key_switch_to_edit" type="text" value="E" />
101 <li><input id="key_switch_to_write" type="text" value="m" />
102 <li><input id="key_switch_to_name_thing" type="text" value="N" />
103 <li><input id="key_switch_to_password" type="text" value="P" />
104 <li><input id="key_switch_to_admin_enter" type="text" value="A" />
105 <li><input id="key_switch_to_control_pw_type" type="text" value="C" />
106 <li><input id="key_switch_to_control_tile_type" type="text" value="Q" />
107 <li><input id="key_switch_to_annotate" type="text" value="M" />
108 <li><input id="key_switch_to_portal" type="text" value="T" />
109 <li>toggle map view: <input id="key_toggle_map_mode" type="text" value="L" />
110 <li>toggle protection character drawing: <input id="key_toggle_tile_draw" type="text" value="m" />
115 let websocket_location = "wss://plomlompom.com/rogue_chat/";
116 //let websocket_location = "ws://localhost:8000/";
121 'long': 'This mode allows you to interact with the map in various ways.'
125 '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. Toggle the map view to show or hide different information layers.'},
127 'short': 'world edit',
128 'long': 'This mode allows you to change the game world in various ways. Individual map tiles can be protected by "protection characters", which you can see by toggling into the protections map view. You can edit a tile if you set the map edit password that matches its protection character. The character "." marks the absence of protection: Such tiles can always be edited.'
131 'short': 'name thing',
132 'long': 'Give name to/change name of thing here.'
135 'short': 'change terrain',
136 '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.'
139 'short': 'change protection character password',
140 'long': 'This mode is the first of two steps to change the password for a tile protection character. First enter the tile protection character for which you want to change the password.'
143 'short': 'change tiles protection password',
144 'long': 'This mode is the second of two steps to change the password for a tile protection character. Enter the new password for the tile protection character you chose.'
146 'control_tile_type': {
147 'short': 'change tiles protection',
148 'long': 'This mode is the first of two steps to change tile protection areas on the map. First enter the tile tile protection character you want to write.'
150 'control_tile_draw': {
151 'short': 'change tiles protection',
152 'long': 'This mode is the second of two steps to change tile protection areas on the map. Toggle tile protection drawing on/off and move the ?? cursor around the map to draw the selected tile protection character.'
155 'short': 'annotate tile',
156 '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.'
159 'short': 'edit portal',
160 '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.'
164 '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:'
168 'long': 'Enter your player name.'
170 'waiting_for_server': {
171 'short': 'waiting for server response',
172 'long': 'Waiting for a server response.'
175 'short': 'waiting for server response',
176 'long': 'Waiting for a server response.'
179 'short': 'set map edit password',
180 '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.'
183 'short': 'become admin',
184 'long': 'This mode allows you to become admin if you know an admin password.'
188 'long': 'This mode allows you access to actions limited to administrators.'
192 let rows_selector = document.getElementById("n_rows");
193 let cols_selector = document.getElementById("n_cols");
194 let key_selectors = document.querySelectorAll('[id^="key_"]');
196 for (const key_switch_selector of document.querySelectorAll('[id^="key_switch_to_"]')) {
197 const action = key_switch_selector.id.slice("key_switch_to_".length);
198 key_switch_selector.parentNode.prepend(mode_helps[action].short + ': ');
201 function restore_selector_value(selector) {
202 let stored_selection = window.localStorage.getItem(selector.id);
203 if (stored_selection) {
204 selector.value = stored_selection;
207 restore_selector_value(rows_selector);
208 restore_selector_value(cols_selector);
209 for (let key_selector of key_selectors) {
210 restore_selector_value(key_selector);
216 initialize: function() {
217 this.rows = rows_selector.value;
218 this.cols = cols_selector.value;
219 this.pre_el = document.getElementById("terminal");
220 this.pre_el.style.color = this.foreground;
221 this.pre_el.style.backgroundColor = this.background;
224 for (let y = 0, x = 0; y <= this.rows; x++) {
225 if (x == this.cols) {
228 this.content.push(line);
230 if (y == this.rows) {
237 blink_screen: function() {
238 this.pre_el.style.color = this.background;
239 this.pre_el.style.backgroundColor = this.foreground;
241 this.pre_el.style.color = this.foreground;
242 this.pre_el.style.backgroundColor = this.background;
245 refresh: function() {
246 function escapeHTML(str) {
248 replace(/&/g, '&').
249 replace(/</g, '<').
250 replace(/>/g, '>').
251 replace(/'/g, ''').
252 replace(/"/g, '"');
254 let pre_content = '';
255 for (let y = 0; y < this.rows; y++) {
256 let line = this.content[y].join('');
258 if (y in tui.links) {
260 for (let span of tui.links[y]) {
261 chunks.push(escapeHTML(line.slice(start_x, span[0])));
262 chunks.push('<a target="_blank" href="');
263 chunks.push(escapeHTML(span[2]));
265 chunks.push(escapeHTML(line.slice(span[0], span[1])));
269 chunks.push(escapeHTML(line.slice(start_x)));
271 chunks = [escapeHTML(line)];
273 for (const chunk of chunks) {
274 pre_content += chunk;
278 this.pre_el.innerHTML = pre_content;
280 write: function(start_y, start_x, msg) {
281 for (let x = start_x, i = 0; x < this.cols && i < msg.length; x++, i++) {
282 this.content[start_y][x] = msg[i];
285 drawBox: function(start_y, start_x, height, width) {
286 let end_y = start_y + height;
287 let end_x = start_x + width;
288 for (let y = start_y, x = start_x; y < this.rows; x++) {
296 this.content[y][x] = ' ';
300 terminal.initialize();
303 tokenize: function(str) {
308 for (let i = 0; i < str.length; i++) {
314 } else if (c == '\\') {
316 } else if (c == '"') {
321 } else if (c == '"') {
323 } else if (c === ' ') {
324 if (token.length > 0) {
332 if (token.length > 0) {
337 parse_yx: function(position_string) {
338 let coordinate_strings = position_string.split(',')
339 let position = [0, 0];
340 position[0] = parseInt(coordinate_strings[0].slice(2));
341 position[1] = parseInt(coordinate_strings[1].slice(2));
353 init: function(url) {
355 this.websocket = new WebSocket(this.url);
356 this.websocket.onopen = function(event) {
357 server.connected = true;
358 game.thing_types = {};
360 server.send(['TASKS']);
361 server.send(['TERRAINS']);
362 server.send(['THING_TYPES']);
363 tui.log_msg("@ server connected! :)");
364 tui.switch_mode('login');
366 this.websocket.onclose = function(event) {
367 server.connected = false;
368 tui.switch_mode('waiting_for_server');
369 tui.log_msg("@ server disconnected :(");
371 this.websocket.onmessage = this.handle_event;
373 reconnect_to: function(url) {
374 this.websocket.close();
377 send: function(tokens) {
378 this.websocket.send(unparser.untokenize(tokens));
380 handle_event: function(event) {
381 let tokens = parser.tokenize(event.data);
382 if (tokens[0] === 'TURN') {
383 game.turn_complete = false;
384 explorer.empty_info_db();
387 game.turn = parseInt(tokens[1]);
388 } else if (tokens[0] === 'THING') {
389 let t = game.get_thing(tokens[3], true);
390 t.position = parser.parse_yx(tokens[1]);
392 } else if (tokens[0] === 'THING_NAME') {
393 let t = game.get_thing(tokens[1], false);
397 } else if (tokens[0] === 'THING_CHAR') {
398 let t = game.get_thing(tokens[1], false);
400 t.player_char = tokens[2];
402 } else if (tokens[0] === 'TASKS') {
403 game.tasks = tokens[1].split(',');
404 tui.mode_write.legal = game.tasks.includes('WRITE');
405 } else if (tokens[0] === 'THING_TYPE') {
406 game.thing_types[tokens[1]] = tokens[2]
407 } else if (tokens[0] === 'TERRAIN') {
408 game.terrains[tokens[1]] = tokens[2]
409 } else if (tokens[0] === 'MAP') {
410 game.map_geometry = tokens[1];
412 game.map_size = parser.parse_yx(tokens[2]);
414 } else if (tokens[0] === 'FOV') {
416 } else if (tokens[0] === 'MAP_CONTROL') {
417 game.map_control = tokens[1]
418 } else if (tokens[0] === 'GAME_STATE_COMPLETE') {
419 game.turn_complete = true;
420 if (tui.mode.name == 'post_login_wait') {
421 tui.switch_mode('play');
422 } else if (tui.mode.name == 'study') {
423 explorer.query_info();
426 } else if (tokens[0] === 'CHAT') {
427 tui.log_msg('# ' + tokens[1], 1);
428 } else if (tokens[0] === 'PLAYER_ID') {
429 game.player_id = parseInt(tokens[1]);
430 } else if (tokens[0] === 'LOGIN_OK') {
431 this.send(['GET_GAMESTATE']);
432 tui.switch_mode('post_login_wait');
433 } else if (tokens[0] === 'ADMIN_OK') {
435 tui.log_msg('@ you now have admin rights');
436 tui.switch_mode('admin');
437 } else if (tokens[0] === 'PORTAL') {
438 let position = parser.parse_yx(tokens[1]);
439 game.portals[position] = tokens[2];
440 } else if (tokens[0] === 'ANNOTATION_HINT') {
441 let position = parser.parse_yx(tokens[1]);
442 explorer.info_hints = explorer.info_hints.concat([position]);
443 } else if (tokens[0] === 'ANNOTATION') {
444 let position = parser.parse_yx(tokens[1]);
445 explorer.update_info_db(position, tokens[2]);
447 } else if (tokens[0] === 'UNHANDLED_INPUT') {
448 tui.log_msg('? unknown command');
449 } else if (tokens[0] === 'PLAY_ERROR') {
450 tui.log_msg('? ' + tokens[1]);
451 terminal.blink_screen();
452 } else if (tokens[0] === 'ARGUMENT_ERROR') {
453 tui.log_msg('? syntax error: ' + tokens[1]);
454 } else if (tokens[0] === 'GAME_ERROR') {
455 tui.log_msg('? game error: ' + tokens[1]);
456 } else if (tokens[0] === 'PONG') {
459 tui.log_msg('? unhandled input: ' + event.data);
465 quote: function(str) {
467 for (let i = 0; i < str.length; i++) {
469 if (['"', '\\'].includes(c)) {
475 return quoted.join('');
477 to_yx: function(yx_coordinate) {
478 return "Y:" + yx_coordinate[0] + ",X:" + yx_coordinate[1];
480 untokenize: function(tokens) {
481 let quoted_tokens = [];
482 for (let token of tokens) {
483 quoted_tokens.push(this.quote(token));
485 return quoted_tokens.join(" ");
490 constructor(name, has_input_prompt=false, shows_info=false,
491 is_intro=false, is_single_char_entry=false) {
493 this.short_desc = mode_helps[name].short;
494 this.available_modes = [];
495 this.has_input_prompt = has_input_prompt;
496 this.shows_info= shows_info;
497 this.is_intro = is_intro;
498 this.help_intro = mode_helps[name].long;
499 this.is_single_char_entry = is_single_char_entry;
502 *iter_available_modes() {
503 for (let mode_name of this.available_modes) {
504 let mode = tui['mode_' + mode_name];
508 let key = tui.keys['switch_to_' + mode.name];
512 list_available_modes() {
514 if (this.available_modes.length > 0) {
515 msg += 'Other modes available from here:\n';
516 for (let [mode, key] of this.iter_available_modes()) {
517 msg += '[' + key + '] – ' + mode.short_desc + '\n';
522 mode_switch_on_key(key_event) {
523 for (let [mode, key] of this.iter_available_modes()) {
524 if (key_event.key == key) {
525 event.preventDefault();
526 tui.switch_mode(mode.name);
538 window_width: terminal.cols / 2,
546 mode_waiting_for_server: new Mode('waiting_for_server',
548 mode_login: new Mode('login', true, false, true),
549 mode_post_login_wait: new Mode('post_login_wait'),
550 mode_chat: new Mode('chat', true),
551 mode_annotate: new Mode('annotate', true, true),
552 mode_play: new Mode('play'),
553 mode_study: new Mode('study', false, true),
554 mode_write: new Mode('write', false, false, false, true),
555 mode_edit: new Mode('edit'),
556 mode_control_pw_type: new Mode('control_pw_type', true),
557 mode_portal: new Mode('portal', true, true),
558 mode_password: new Mode('password', true),
559 mode_name_thing: new Mode('name_thing', true, true),
560 mode_admin_enter: new Mode('admin_enter', true),
561 mode_admin: new Mode('admin'),
562 mode_control_pw_pw: new Mode('control_pw_pw', true),
563 mode_control_tile_type: new Mode('control_tile_type', true),
564 mode_control_tile_draw: new Mode('control_tile_draw'),
566 this.mode_play.available_modes = ["chat", "study", "edit", "admin_enter"]
567 this.mode_study.available_modes = ["chat", "play", "admin_enter", "edit"]
568 this.mode_admin.available_modes = ["control_pw_type",
569 "control_tile_type", "chat",
570 "study", "play", "edit"]
571 this.mode_control_tile_draw.available_modes = ["admin_enter"]
572 this.mode_edit.available_modes = ["write", "annotate", "portal", "name_thing",
573 "password", "chat", "study", "play",
575 this.mode = this.mode_waiting_for_server;
576 this.inputEl = document.getElementById("input");
577 this.inputEl.focus();
578 this.recalc_input_lines();
579 this.height_header = this.height_turn_line + this.height_mode_line;
580 this.log_msg("@ waiting for server connection ...");
583 init_keys: function() {
585 for (let key_selector of key_selectors) {
586 this.keys[key_selector.id.slice(4)] = key_selector.value;
588 if (game.map_geometry == 'Square') {
589 this.movement_keys = {
590 [this.keys.square_move_up]: 'UP',
591 [this.keys.square_move_left]: 'LEFT',
592 [this.keys.square_move_down]: 'DOWN',
593 [this.keys.square_move_right]: 'RIGHT'
595 document.getElementById("move_upright").hidden = true;
596 document.getElementById("move_upleft").hidden = true;
597 document.getElementById("move_downright").hidden = true;
598 document.getElementById("move_downleft").hidden = true;
599 document.getElementById("move_up").hidden = false;
600 document.getElementById("move_down").hidden = false;
601 } else if (game.map_geometry == 'Hex') {
602 document.getElementById("move_upright").hidden = false;
603 document.getElementById("move_upleft").hidden = false;
604 document.getElementById("move_downright").hidden = false;
605 document.getElementById("move_downleft").hidden = false;
606 document.getElementById("move_up").hidden = true;
607 document.getElementById("move_down").hidden = true;
608 this.movement_keys = {
609 [this.keys.hex_move_upleft]: 'UPLEFT',
610 [this.keys.hex_move_upright]: 'UPRIGHT',
611 [this.keys.hex_move_right]: 'RIGHT',
612 [this.keys.hex_move_downright]: 'DOWNRIGHT',
613 [this.keys.hex_move_downleft]: 'DOWNLEFT',
614 [this.keys.hex_move_left]: 'LEFT'
618 switch_mode: function(mode_name) {
619 if (this.mode.name == 'control_tile_draw') {
620 tui.log_msg('@ finished tile protection drawing.')
622 this.tile_draw = false;
623 if (mode_name == 'admin_enter' && this.is_admin) {
625 } else if (mode_name == 'name_thing') {
626 let player_position = game.things[game.player_id].position;
628 for (let t_id in game.things) {
629 if (t_id == game.player_id) {
632 let t = game.things[t_id];
633 if (player_position[0] == t.position[0] && player_position[1] == t.position[1]) {
639 terminal.blink_screen();
640 this.log_msg('? not standing over thing');
643 this.selected_thing_id = thing_id;
646 this.mode = this['mode_' + mode_name];
647 if (["control_tile_draw", "control_tile_type", "control_pw_type"].includes(this.mode.name)) {
648 this.map_mode = 'protections';
649 } else if (this.mode.name != "edit") {
650 this.map_mode = 'terrain + things';
652 if (this.mode.has_input_prompt || this.mode.is_single_char_entry) {
653 this.inputEl.focus();
655 if (game.player_id in game.things && (this.mode.shows_info || this.mode.name == 'control_tile_draw')) {
656 explorer.position = game.things[game.player_id].position;
657 if (this.mode.shows_info) {
658 explorer.query_info();
662 this.restore_input_values();
663 for (let el of document.getElementsByTagName("button")) {
666 document.getElementById("help").disabled = false;
667 if (this.mode.name == 'play' || this.mode.name == 'study' || this.mode.name == 'control_tile_draw' || this.mode.name == 'edit') {
668 for (const move_key of document.querySelectorAll('[id^="move_"]')) {
669 move_key.disabled = false;
672 if (!this.mode.is_intro && this.mode.name != 'play') {
673 document.getElementById("switch_to_play").disabled = false;
675 if (!this.mode.is_intro && this.mode.name != 'study') {
676 document.getElementById("switch_to_study").disabled = false;
678 if (!this.mode.is_intro && this.mode.name != 'chat') {
679 document.getElementById("switch_to_chat").disabled = false;
681 if (!this.mode.is_intro && this.mode.name != 'edit') {
682 document.getElementById("switch_to_edit").disabled = false;
684 if (!this.mode.is_intro && this.mode.name != 'admin' && this.mode.name != 'admin_enter') {
685 document.getElementById("switch_to_admin_enter").disabled = false;
687 if (this.mode.name == 'login') {
688 if (this.login_name) {
689 server.send(['LOGIN', this.login_name]);
691 this.log_msg("? need login name");
693 } else if (this.mode.name == 'play') {
694 if (game.tasks.includes('PICK_UP')) {
695 document.getElementById("take_thing").disabled = false;
697 if (game.tasks.includes('DROP')) {
698 document.getElementById("drop_thing").disabled = false;
700 if (game.tasks.includes('MOVE')) {
702 document.getElementById("teleport").disabled = false;
703 } else if (this.mode.name == 'edit') {
704 if (game.tasks.includes('FLATTEN_SURROUNDINGS')) {
705 document.getElementById("flatten").disabled = false;
707 document.getElementById("switch_to_annotate").disabled = false;
708 document.getElementById("switch_to_write").disabled = false;
709 document.getElementById("switch_to_portal").disabled = false;
710 document.getElementById("switch_to_password").disabled = false;
711 document.getElementById("switch_to_name_thing").disabled = false;
712 document.getElementById("toggle_map_mode").disabled = false;
713 } else if (this.mode.name == 'admin') {
714 document.getElementById("switch_to_control_pw_type").disabled = false;
715 document.getElementById("switch_to_control_tile_type").disabled = false;
716 } else if (this.mode.name == 'study') {
717 document.getElementById("toggle_map_mode").disabled = false;
718 } else if (this.mode.is_single_char_entry) {
719 this.show_help = true;
720 } else if (this.mode.name == 'admin_enter') {
721 this.log_msg('@ enter admin password:')
722 } else if (this.mode.name == 'control_pw_type') {
723 this.log_msg('@ enter tile protection character for which you want to change the password:')
724 } else if (this.mode.name == 'control_tile_type') {
725 this.log_msg('@ enter tile protection character which you want to draw:')
726 } else if (this.mode.name == 'control_pw_pw') {
727 this.log_msg('@ enter tile protection password for "' + this.tile_control_char + '":');
728 } else if (this.mode.name == 'control_tile_draw') {
729 document.getElementById("toggle_tile_draw").disabled = false;
730 this.log_msg('@ can draw tile protection character "' + this.tile_control_char + '", turn drawing on/off with [' + this.keys.toggle_tile_draw + '], finish with [' + this.keys.switch_to_admin_enter + '].')
734 offset_links: function(offset, links) {
735 for (let y in links) {
736 let real_y = offset[0] + parseInt(y);
737 if (!this.links[real_y]) {
738 this.links[real_y] = [];
740 for (let link of links[y]) {
741 const offset_link = [link[0] + offset[1], link[1] + offset[1], link[2]];
742 this.links[real_y].push(offset_link);
746 restore_input_values: function() {
747 if (this.mode.name == 'annotate' && explorer.position in explorer.info_db) {
748 let info = explorer.info_db[explorer.position];
749 if (info != "(none)") {
750 this.inputEl.value = info;
751 this.recalc_input_lines();
753 } else if (this.mode.name == 'portal' && explorer.position in game.portals) {
754 let portal = game.portals[explorer.position]
755 this.inputEl.value = portal;
756 this.recalc_input_lines();
757 } else if (this.mode.name == 'password') {
758 this.inputEl.value = this.password;
759 this.recalc_input_lines();
760 } else if (this.mode.name == 'name_thing') {
761 let t = game.get_thing(this.selected_thing_id);
763 this.inputEl.value = t.name_;
764 this.recalc_input_lines();
768 empty_input: function(str) {
769 this.inputEl.value = "";
770 if (this.mode.has_input_prompt) {
771 this.recalc_input_lines();
773 this.height_input = 0;
776 recalc_input_lines: function() {
778 [this.input_lines, _] = this.msg_into_lines_of_width(this.input_prompt + this.inputEl.value, this.window_width);
779 this.height_input = this.input_lines.length;
781 msg_into_lines_of_width: function(msg, width) {
782 function push_inner_link(y, end_x) {
783 if (!inner_links[y]) {
786 inner_links[y].push([url_start_x, end_x, url]);
788 const matches = msg.matchAll(/https?:\/\/[^\s]+/g)
791 for (const match of matches) {
792 const url = match[0];
793 const url_start = match.index;
794 const url_end = match.index + match[0].length;
795 link_data[url_start] = url;
796 url_ends.push(url_end);
800 let inner_links = {};
804 for (let i = 0, x = 0, y = 0; i < msg.length; i++, x++) {
805 if (x >= width || msg[i] == "\n") {
807 push_inner_link(y, chunk.length);
813 if (msg[i] == "\n") {
818 if (msg[i] != "\n") {
821 if (i in link_data) {
825 } else if (url_ends.includes(i)) {
826 push_inner_link(y, x);
832 push_inner_link(lines.length - 1, chunk.length);
834 return [lines, inner_links];
836 log_msg: function(msg) {
838 while (this.log.length > 100) {
843 draw_map: function() {
844 let map_lines_split = [];
846 for (let i = 0, j = 0; i < game.map.length; i++, j++) {
847 if (j == game.map_size[1]) {
848 map_lines_split.push(line);
852 if (this.map_mode == 'protections') {
853 line.push(game.map_control[i] + ' ');
855 line.push(game.map[i] + ' ');
858 map_lines_split.push(line);
859 if (this.map_mode == 'terrain + annotations') {
860 for (const coordinate of explorer.info_hints) {
861 map_lines_split[coordinate[0]][coordinate[1]] = 'A ';
863 } else if (this.map_mode == 'terrain + things') {
864 for (const p in game.portals) {
865 let coordinate = p.split(',')
866 let original = map_lines_split[coordinate[0]][coordinate[1]];
867 map_lines_split[coordinate[0]][coordinate[1]] = original[0] + 'P';
869 let used_positions = [];
870 for (const thing_id in game.things) {
871 let t = game.things[thing_id];
872 let symbol = game.thing_types[t.type_];
875 meta_char = t.player_char;
877 if (used_positions.includes(t.position.toString())) {
880 map_lines_split[t.position[0]][t.position[1]] = symbol + meta_char;
881 used_positions.push(t.position.toString());
884 let player = game.things[game.player_id];
885 if (tui.mode.shows_info || tui.mode.name == 'control_tile_draw') {
886 map_lines_split[explorer.position[0]][explorer.position[1]] = '??';
887 } else if (tui.map_mode != 'terrain + things') {
888 map_lines_split[player.position[0]][player.position[1]] = '??';
891 if (game.map_geometry == 'Square') {
892 for (let line_split of map_lines_split) {
893 map_lines.push(line_split.join(''));
895 } else if (game.map_geometry == 'Hex') {
897 for (let line_split of map_lines_split) {
898 map_lines.push(' '.repeat(indent) + line_split.join(''));
906 let window_center = [terminal.rows / 2, this.window_width / 2];
907 let center_position = [player.position[0], player.position[1]];
908 if (tui.mode.shows_info || tui.mode.name == 'control_tile_draw') {
909 center_position = [explorer.position[0], explorer.position[1]];
911 center_position[1] = center_position[1] * 2;
912 let offset = [center_position[0] - window_center[0],
913 center_position[1] - window_center[1]]
914 if (game.map_geometry == 'Hex' && offset[0] % 2) {
917 let term_y = Math.max(0, -offset[0]);
918 let term_x = Math.max(0, -offset[1]);
919 let map_y = Math.max(0, offset[0]);
920 let map_x = Math.max(0, offset[1]);
921 for (; term_y < terminal.rows && map_y < game.map_size[0]; term_y++, map_y++) {
922 let to_draw = map_lines[map_y].slice(map_x, this.window_width + offset[1]);
923 terminal.write(term_y, term_x, to_draw);
926 draw_mode_line: function() {
927 let help = 'hit [' + this.keys.help + '] for help';
928 if (this.mode.has_input_prompt) {
929 help = 'enter /help for help';
931 terminal.write(0, this.window_width, 'MODE: ' + this.mode.short_desc + ' – ' + help);
933 draw_turn_line: function(n) {
934 terminal.write(1, this.window_width, 'TURN: ' + game.turn);
936 draw_history: function() {
937 let log_display_lines = [];
939 let y_offset_in_log = 0;
940 for (let line of this.log) {
941 let [new_lines, link_data] = this.msg_into_lines_of_width(line,
943 log_display_lines = log_display_lines.concat(new_lines);
944 for (const y in link_data) {
945 const rel_y = y_offset_in_log + parseInt(y);
946 log_links[rel_y] = [];
947 for (let link of link_data[y]) {
948 log_links[rel_y].push(link);
951 y_offset_in_log += new_lines.length;
953 let i = log_display_lines.length - 1;
954 for (let y = terminal.rows - 1 - this.height_input;
955 y >= this.height_header && i >= 0;
957 terminal.write(y, this.window_width, log_display_lines[i]);
959 for (const key of Object.keys(log_links)) {
960 if (parseInt(key) <= i) {
961 delete log_links[key];
964 let offset = [terminal.rows - this.height_input - log_display_lines.length,
966 this.offset_links(offset, log_links);
968 draw_info: function() {
969 let [lines, link_data] = this.msg_into_lines_of_width(explorer.get_info(),
971 let offset = [this.height_header, this.window_width];
972 for (let y = offset[0], i = 0; y < terminal.rows && i < lines.length; y++, i++) {
973 terminal.write(y, offset[1], lines[i]);
975 this.offset_links(offset, link_data);
977 draw_input: function() {
978 if (this.mode.has_input_prompt) {
979 for (let y = terminal.rows - this.height_input, i = 0; i < this.input_lines.length; y++, i++) {
980 terminal.write(y, this.window_width, this.input_lines[i]);
984 draw_help: function() {
985 let movement_keys_desc = '';
986 if (!this.mode.is_intro) {
987 movement_keys_desc = Object.keys(this.movement_keys).join(',');
989 let content = this.mode.short_desc + " help\n\n" + this.mode.help_intro + "\n\n";
990 if (this.mode.name == 'play') {
991 content += "Available actions:\n";
992 if (game.tasks.includes('MOVE')) {
993 content += "[" + movement_keys_desc + "] – move player\n";
995 if (game.tasks.includes('PICK_UP')) {
996 content += "[" + this.keys.take_thing + "] – pick up thing\n";
998 if (game.tasks.includes('DROP')) {
999 content += "[" + this.keys.drop_thing + "] – drop thing\n";
1001 content += "[" + tui.keys.teleport + "] – teleport\n";
1003 } else if (this.mode.name == 'study') {
1004 content += "Available actions:\n";
1005 content += '[' + movement_keys_desc + '] – move question mark\n';
1006 content += '[' + this.keys.toggle_map_mode + '] – toggle map view\n';
1008 } else if (this.mode.name == 'edit') {
1009 content += "Available actions:\n";
1010 if (game.tasks.includes('MOVE')) {
1011 content += "[" + movement_keys_desc + "] – move player\n";
1013 if (game.tasks.includes('FLATTEN_SURROUNDINGS')) {
1014 content += "[" + tui.keys.flatten + "] – flatten surroundings\n";
1016 content += '[' + this.keys.toggle_map_mode + '] – toggle map view\n';
1018 } else if (this.mode.name == 'control_tile_draw') {
1019 content += "Available actions:\n";
1020 content += "[" + tui.keys.toggle_tile_draw + "] – toggle protection character drawing\n";
1022 } else if (this.mode.name == 'chat') {
1023 content += '/nick NAME – re-name yourself to NAME\n';
1024 content += '/' + this.keys.switch_to_play + ' or /play – switch to play mode\n';
1025 content += '/' + this.keys.switch_to_study + ' or /study – switch to study mode\n';
1026 content += '/' + this.keys.switch_to_edit + ' or /edit – switch to map edit mode\n';
1027 content += '/' + this.keys.switch_to_admin_enter + ' or /admin – switch to admin mode\n';
1029 content += this.mode.list_available_modes();
1031 if (!this.mode.has_input_prompt) {
1032 start_x = this.window_width
1034 terminal.drawBox(0, start_x, terminal.rows, this.window_width);
1035 let [lines, _] = this.msg_into_lines_of_width(content, this.window_width);
1036 for (let y = 0, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
1037 terminal.write(y, start_x, lines[i]);
1040 toggle_tile_draw: function() {
1041 if (tui.tile_draw) {
1042 tui.tile_draw = false;
1044 tui.tile_draw = true;
1047 toggle_map_mode: function() {
1048 if (tui.map_mode == 'terrain only') {
1049 tui.map_mode = 'terrain + annotations';
1050 } else if (tui.map_mode == 'terrain + annotations') {
1051 tui.map_mode = 'terrain + things';
1052 } else if (tui.map_mode == 'terrain + things') {
1053 tui.map_mode = 'protections';
1054 } else if (tui.map_mode == 'protections') {
1055 tui.map_mode = 'terrain only';
1058 full_refresh: function() {
1060 terminal.drawBox(0, 0, terminal.rows, terminal.cols);
1061 if (this.mode.is_intro) {
1062 this.draw_history();
1065 if (game.turn_complete) {
1067 this.draw_turn_line();
1069 this.draw_mode_line();
1070 if (this.mode.shows_info) {
1073 this.draw_history();
1077 if (this.show_help) {
1089 this.map_control = "";
1090 this.map_size = [0,0];
1091 this.player_id = -1;
1095 get_thing: function(id_, create_if_not_found=false) {
1096 if (id_ in game.things) {
1097 return game.things[id_];
1098 } else if (create_if_not_found) {
1099 let t = new Thing([0,0]);
1100 game.things[id_] = t;
1104 move: function(start_position, direction) {
1105 let target = [start_position[0], start_position[1]];
1106 if (direction == 'LEFT') {
1108 } else if (direction == 'RIGHT') {
1110 } else if (game.map_geometry == 'Square') {
1111 if (direction == 'UP') {
1113 } else if (direction == 'DOWN') {
1116 } else if (game.map_geometry == 'Hex') {
1117 let start_indented = start_position[0] % 2;
1118 if (direction == 'UPLEFT') {
1120 if (!start_indented) {
1123 } else if (direction == 'UPRIGHT') {
1125 if (start_indented) {
1128 } else if (direction == 'DOWNLEFT') {
1130 if (!start_indented) {
1133 } else if (direction == 'DOWNRIGHT') {
1135 if (start_indented) {
1140 if (target[0] < 0 || target[1] < 0 ||
1141 target[0] >= this.map_size[0] || target[1] >= this.map_size[1]) {
1146 teleport: function() {
1147 let player = this.get_thing(game.player_id);
1148 if (player.position in this.portals) {
1149 server.reconnect_to(this.portals[player.position]);
1151 terminal.blink_screen();
1152 tui.log_msg('? not standing on portal')
1160 server.init(websocket_location);
1166 move: function(direction) {
1167 let target = game.move(this.position, direction);
1169 this.position = target
1170 if (tui.mode.shows_info) {
1172 } else if (tui.tile_draw) {
1173 this.send_tile_control_command();
1176 terminal.blink_screen();
1179 update_info_db: function(yx, str) {
1180 this.info_db[yx] = str;
1181 if (tui.mode.name == 'study') {
1185 empty_info_db: function() {
1187 this.info_hints = [];
1188 if (tui.mode.name == 'study') {
1192 query_info: function() {
1193 server.send(["GET_ANNOTATION", unparser.to_yx(explorer.position)]);
1195 get_info: function() {
1196 let info = "MAP VIEW: " + tui.map_mode + "\n";
1197 let position_i = this.position[0] * game.map_size[1] + this.position[1];
1198 if (game.fov[position_i] != '.') {
1199 return info + 'outside field of view';
1201 let terrain_char = game.map[position_i]
1202 let terrain_desc = '?'
1203 if (game.terrains[terrain_char]) {
1204 terrain_desc = game.terrains[terrain_char];
1206 info += 'TERRAIN: "' + terrain_char + '" / ' + terrain_desc + "\n";
1207 let protection = game.map_control[position_i];
1208 if (protection == '.') {
1209 protection = 'unprotected';
1211 info += 'PROTECTION: ' + protection + '\n';
1212 for (let t_id in game.things) {
1213 let t = game.things[t_id];
1214 if (t.position[0] == this.position[0] && t.position[1] == this.position[1]) {
1215 let symbol = game.thing_types[t.type_];
1216 info += "THING: " + t.type_ + " / " + symbol;
1217 if (t.player_char) {
1218 info += t.player_char;
1221 info += " (" + t.name_ + ")";
1226 if (this.position in game.portals) {
1227 info += "PORTAL: " + game.portals[this.position] + "\n";
1229 if (this.position in this.info_db) {
1230 info += "ANNOTATIONS: " + this.info_db[this.position];
1232 info += 'waiting …';
1236 annotate: function(msg) {
1237 if (msg.length == 0) {
1238 msg = " "; // triggers annotation deletion
1240 server.send(["ANNOTATE", unparser.to_yx(explorer.position), msg, tui.password]);
1242 set_portal: function(msg) {
1243 if (msg.length == 0) {
1244 msg = " "; // triggers portal deletion
1246 server.send(["PORTAL", unparser.to_yx(explorer.position), msg, tui.password]);
1248 send_tile_control_command: function() {
1249 server.send(["SET_TILE_CONTROL", unparser.to_yx(this.position), tui.tile_control_char]);
1253 tui.inputEl.addEventListener('input', (event) => {
1254 if (tui.mode.has_input_prompt) {
1255 let max_length = tui.window_width * terminal.rows - tui.input_prompt.length;
1256 if (tui.inputEl.value.length > max_length) {
1257 tui.inputEl.value = tui.inputEl.value.slice(0, max_length);
1259 tui.recalc_input_lines();
1260 } else if (tui.mode.name == 'write' && tui.inputEl.value.length > 0) {
1261 server.send(["TASK:WRITE", tui.inputEl.value[0], tui.password]);
1262 tui.switch_mode('edit');
1266 document.onclick = function() {
1267 tui.show_help = false;
1269 tui.inputEl.addEventListener('keydown', (event) => {
1270 tui.show_help = false;
1271 if (event.key == 'Enter') {
1272 event.preventDefault();
1274 if (tui.mode.has_input_prompt && event.key == 'Enter' && tui.inputEl.value == '/help') {
1275 tui.show_help = true;
1277 tui.restore_input_values();
1278 } else if (!tui.mode.has_input_prompt && event.key == tui.keys.help
1279 && !tui.mode.is_single_char_entry) {
1280 tui.show_help = true;
1281 } else if (tui.mode.name == 'login' && event.key == 'Enter') {
1282 tui.login_name = tui.inputEl.value;
1283 server.send(['LOGIN', tui.inputEl.value]);
1285 } else if (tui.mode.name == 'control_pw_pw' && event.key == 'Enter') {
1286 if (tui.inputEl.value.length == 0) {
1287 tui.log_msg('@ aborted');
1289 server.send(['SET_MAP_CONTROL_PASSWORD',
1290 tui.tile_control_char, tui.inputEl.value]);
1291 tui.log_msg('@ sent new password for protection character "' + tui.tile_control_char + '".');
1293 tui.switch_mode('admin');
1294 } else if (tui.mode.name == 'portal' && event.key == 'Enter') {
1295 explorer.set_portal(tui.inputEl.value);
1296 tui.switch_mode('edit');
1297 } else if (tui.mode.name == 'name_thing' && event.key == 'Enter') {
1298 if (tui.inputEl.value.length == 0) {
1299 tui.inputEl.value = " ";
1301 server.send(["THING_NAME", tui.selected_thing_id, tui.inputEl.value]);
1302 tui.switch_mode('edit');
1303 } else if (tui.mode.name == 'annotate' && event.key == 'Enter') {
1304 explorer.annotate(tui.inputEl.value);
1305 tui.switch_mode('edit');
1306 } else if (tui.mode.name == 'password' && event.key == 'Enter') {
1307 if (tui.inputEl.value.length == 0) {
1308 tui.inputEl.value = " ";
1310 tui.password = tui.inputEl.value
1311 tui.switch_mode('edit');
1312 } else if (tui.mode.name == 'admin_enter' && event.key == 'Enter') {
1313 server.send(['BECOME_ADMIN', tui.inputEl.value]);
1314 tui.switch_mode('play');
1315 } else if (tui.mode.name == 'control_pw_type' && event.key == 'Enter') {
1316 if (tui.inputEl.value.length != 1) {
1317 tui.log_msg('@ entered non-single-char, therefore aborted');
1318 tui.switch_mode('admin');
1320 tui.tile_control_char = tui.inputEl.value[0];
1321 tui.switch_mode('control_pw_pw');
1323 } else if (tui.mode.name == 'control_tile_type' && event.key == 'Enter') {
1324 if (tui.inputEl.value.length != 1) {
1325 tui.log_msg('@ entered non-single-char, therefore aborted');
1326 tui.switch_mode('admin');
1328 tui.tile_control_char = tui.inputEl.value[0];
1329 tui.switch_mode('control_tile_draw');
1331 } else if (tui.mode.name == 'chat' && event.key == 'Enter') {
1332 let tokens = parser.tokenize(tui.inputEl.value);
1333 if (tokens.length > 0 && tokens[0].length > 0) {
1334 if (tui.inputEl.value[0][0] == '/') {
1335 if (tokens[0].slice(1) == 'play' || tokens[0][1] == tui.keys.switch_to_play) {
1336 tui.switch_mode('play');
1337 } else if (tokens[0].slice(1) == 'study' || tokens[0][1] == tui.keys.switch_to_study) {
1338 tui.switch_mode('study');
1339 } else if (tokens[0].slice(1) == 'edit' || tokens[0][1] == tui.keys.switch_to_edit) {
1340 tui.switch_mode('edit');
1341 } else if (tokens[0].slice(1) == 'admin' || tokens[0][1] == tui.keys.switch_to_admin_enter) {
1342 tui.switch_mode('admin_enter');
1343 } else if (tokens[0].slice(1) == 'nick') {
1344 if (tokens.length > 1) {
1345 server.send(['NICK', tokens[1]]);
1347 tui.log_msg('? need new name');
1350 tui.log_msg('? unknown command');
1353 server.send(['ALL', tui.inputEl.value]);
1355 } else if (tui.inputEl.valuelength > 0) {
1356 server.send(['ALL', tui.inputEl.value]);
1359 } else if (tui.mode.name == 'play') {
1360 if (tui.mode.mode_switch_on_key(event)) {
1362 } else if (event.key === tui.keys.take_thing
1363 && game.tasks.includes('PICK_UP')) {
1364 server.send(["TASK:PICK_UP"]);
1365 } else if (event.key === tui.keys.drop_thing
1366 && game.tasks.includes('DROP')) {
1367 server.send(["TASK:DROP"]);
1368 } else if (event.key in tui.movement_keys
1369 && game.tasks.includes('MOVE')) {
1370 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1371 } else if (event.key === tui.keys.teleport) {
1374 } else if (tui.mode.name == 'study') {
1375 if (tui.mode.mode_switch_on_key(event)) {
1377 } else if (event.key in tui.movement_keys) {
1378 explorer.move(tui.movement_keys[event.key]);
1379 } else if (event.key == tui.keys.toggle_map_mode) {
1380 tui.toggle_map_mode();
1382 } else if (tui.mode.name == 'control_tile_draw') {
1383 if (tui.mode.mode_switch_on_key(event)) {
1385 } else if (event.key in tui.movement_keys) {
1386 explorer.move(tui.movement_keys[event.key]);
1387 } else if (event.key === tui.keys.toggle_tile_draw) {
1388 tui.toggle_tile_draw();
1390 } else if (tui.mode.name == 'admin') {
1391 if (tui.mode.mode_switch_on_key(event)) {
1394 } else if (tui.mode.name == 'edit') {
1395 if (tui.mode.mode_switch_on_key(event)) {
1397 } else if (event.key in tui.movement_keys
1398 && game.tasks.includes('MOVE')) {
1399 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1400 } else if (event.key === tui.keys.flatten
1401 && game.tasks.includes('FLATTEN_SURROUNDINGS')) {
1402 server.send(["TASK:FLATTEN_SURROUNDINGS", tui.password]);
1403 } else if (event.key == tui.keys.toggle_map_mode) {
1404 tui.toggle_map_mode();
1410 rows_selector.addEventListener('input', function() {
1411 if (rows_selector.value % 4 != 0 || rows_selector.value < 24) {
1414 window.localStorage.setItem(rows_selector.id, rows_selector.value);
1415 terminal.initialize();
1418 cols_selector.addEventListener('input', function() {
1419 if (cols_selector.value % 4 != 0 || cols_selector.value < 80) {
1422 window.localStorage.setItem(cols_selector.id, cols_selector.value);
1423 terminal.initialize();
1424 tui.window_width = terminal.cols / 2,
1427 for (let key_selector of key_selectors) {
1428 key_selector.addEventListener('input', function() {
1429 window.localStorage.setItem(key_selector.id, key_selector.value);
1433 window.setInterval(function() {
1434 if (server.connected) {
1435 server.send(['PING']);
1437 server.reconnect_to(server.url);
1438 tui.log_msg('@ attempting reconnect …')
1441 window.setInterval(function() {
1443 if (document.activeElement == tui.inputEl) {
1444 val = "on (click outside terminal to change)";
1446 val = "off (click into terminal to change)";
1448 document.getElementById("keyboard_control").textContent = val;
1450 document.getElementById("terminal").onclick = function() {
1451 tui.inputEl.focus();
1453 document.getElementById("help").onclick = function() {
1454 tui.show_help = true;
1457 for (const switchEl of document.querySelectorAll('[id^="switch_to_"]')) {
1458 const mode = switchEl.id.slice("switch_to_".length);
1459 switchEl.onclick = function() {
1460 tui.switch_mode(mode);
1464 document.getElementById("toggle_tile_draw").onclick = function() {
1465 tui.toggle_tile_draw();
1467 document.getElementById("toggle_map_mode").onclick = function() {
1468 tui.toggle_map_mode();
1471 document.getElementById("take_thing").onclick = function() {
1472 server.send(['TASK:PICK_UP']);
1474 document.getElementById("drop_thing").onclick = function() {
1475 server.send(['TASK:DROP']);
1477 document.getElementById("flatten").onclick = function() {
1478 server.send(['TASK:FLATTEN_SURROUNDINGS', tui.password]);
1480 document.getElementById("teleport").onclick = function() {
1483 document.getElementById("move_upleft").onclick = function() {
1484 if (tui.mode.name == 'play' || tui.mode.name == 'edit') {
1485 server.send(['TASK:MOVE', 'UPLEFT']);
1487 explorer.move('UPLEFT');
1490 document.getElementById("move_left").onclick = function() {
1491 if (tui.mode.name == 'play' || tui.mode.name == 'edit') {
1492 server.send(['TASK:MOVE', 'LEFT']);
1494 explorer.move('LEFT');
1497 document.getElementById("move_downleft").onclick = function() {
1498 if (tui.mode.name == 'play' || tui.mode.name == 'edit') {
1499 server.send(['TASK:MOVE', 'DOWNLEFT']);
1501 explorer.move('DOWNLEFT');
1504 document.getElementById("move_down").onclick = function() {
1505 if (tui.mode.name == 'play' || tui.mode.name == 'edit') {
1506 server.send(['TASK:MOVE', 'DOWN']);
1508 explorer.move('DOWN');
1511 document.getElementById("move_up").onclick = function() {
1512 if (tui.mode.name == 'play' || tui.mode.name == 'edit') {
1513 server.send(['TASK:MOVE', 'UP']);
1515 explorer.move('UP');
1518 document.getElementById("move_upright").onclick = function() {
1519 if (tui.mode.name == 'play' || tui.mode.name == 'edit') {
1520 server.send(['TASK:MOVE', 'UPRIGHT']);
1522 explorer.move('UPRIGHT');
1525 document.getElementById("move_right").onclick = function() {
1526 if (tui.mode.name == 'play' || tui.mode.name == 'edit') {
1527 server.send(['TASK:MOVE', 'RIGHT']);
1529 explorer.move('RIGHT');
1532 document.getElementById("move_downright").onclick = function() {
1533 if (tui.mode.name == 'play' || tui.mode.name == 'edit') {
1534 server.send(['TASK:MOVE', 'DOWNRIGHT']);
1536 explorer.move('DOWNRIGHT');