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();
661 this.inputEl.value = "";
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;
752 } else if (this.mode.name == 'portal' && explorer.position in game.portals) {
753 let portal = game.portals[explorer.position]
754 this.inputEl.value = portal;
755 } else if (this.mode.name == 'password') {
756 this.inputEl.value = this.password;
757 } else if (this.mode.name == 'name_thing') {
758 let t = game.get_thing(this.selected_thing_id);
760 this.inputEl.value = t.name_;
764 recalc_input_lines: function() {
765 if (this.mode.has_input_prompt) {
767 [this.input_lines, _] = this.msg_into_lines_of_width(this.input_prompt + this.inputEl.value, this.window_width);
769 this.input_lines = [];
771 this.height_input = this.input_lines.length;
773 msg_into_lines_of_width: function(msg, width) {
774 function push_inner_link(y, end_x) {
775 if (!inner_links[y]) {
778 inner_links[y].push([url_start_x, end_x, url]);
780 const matches = msg.matchAll(/https?:\/\/[^\s]+/g)
783 for (const match of matches) {
784 const url = match[0];
785 const url_start = match.index;
786 const url_end = match.index + match[0].length;
787 link_data[url_start] = url;
788 url_ends.push(url_end);
792 let inner_links = {};
796 for (let i = 0, x = 0, y = 0; i < msg.length; i++, x++) {
797 if (x >= width || msg[i] == "\n") {
799 push_inner_link(y, chunk.length);
805 if (msg[i] == "\n") {
810 if (msg[i] != "\n") {
813 if (i in link_data) {
817 } else if (url_ends.includes(i)) {
818 push_inner_link(y, x);
824 push_inner_link(lines.length - 1, chunk.length);
826 return [lines, inner_links];
828 log_msg: function(msg) {
830 while (this.log.length > 100) {
835 draw_map: function() {
836 let map_lines_split = [];
838 for (let i = 0, j = 0; i < game.map.length; i++, j++) {
839 if (j == game.map_size[1]) {
840 map_lines_split.push(line);
844 if (this.map_mode == 'protections') {
845 line.push(game.map_control[i] + ' ');
847 line.push(game.map[i] + ' ');
850 map_lines_split.push(line);
851 if (this.map_mode == 'terrain + annotations') {
852 for (const coordinate of explorer.info_hints) {
853 map_lines_split[coordinate[0]][coordinate[1]] = 'A ';
855 } else if (this.map_mode == 'terrain + things') {
856 for (const p in game.portals) {
857 let coordinate = p.split(',')
858 let original = map_lines_split[coordinate[0]][coordinate[1]];
859 map_lines_split[coordinate[0]][coordinate[1]] = original[0] + 'P';
861 let used_positions = [];
862 for (const thing_id in game.things) {
863 let t = game.things[thing_id];
864 let symbol = game.thing_types[t.type_];
867 meta_char = t.player_char;
869 if (used_positions.includes(t.position.toString())) {
872 map_lines_split[t.position[0]][t.position[1]] = symbol + meta_char;
873 used_positions.push(t.position.toString());
876 let player = game.things[game.player_id];
877 if (tui.mode.shows_info || tui.mode.name == 'control_tile_draw') {
878 map_lines_split[explorer.position[0]][explorer.position[1]] = '??';
879 } else if (tui.map_mode != 'terrain + things') {
880 map_lines_split[player.position[0]][player.position[1]] = '??';
883 if (game.map_geometry == 'Square') {
884 for (let line_split of map_lines_split) {
885 map_lines.push(line_split.join(''));
887 } else if (game.map_geometry == 'Hex') {
889 for (let line_split of map_lines_split) {
890 map_lines.push(' '.repeat(indent) + line_split.join(''));
898 let window_center = [terminal.rows / 2, this.window_width / 2];
899 let center_position = [player.position[0], player.position[1]];
900 if (tui.mode.shows_info || tui.mode.name == 'control_tile_draw') {
901 center_position = [explorer.position[0], explorer.position[1]];
903 center_position[1] = center_position[1] * 2;
904 let offset = [center_position[0] - window_center[0],
905 center_position[1] - window_center[1]]
906 if (game.map_geometry == 'Hex' && offset[0] % 2) {
909 let term_y = Math.max(0, -offset[0]);
910 let term_x = Math.max(0, -offset[1]);
911 let map_y = Math.max(0, offset[0]);
912 let map_x = Math.max(0, offset[1]);
913 for (; term_y < terminal.rows && map_y < game.map_size[0]; term_y++, map_y++) {
914 let to_draw = map_lines[map_y].slice(map_x, this.window_width + offset[1]);
915 terminal.write(term_y, term_x, to_draw);
918 draw_mode_line: function() {
919 let help = 'hit [' + this.keys.help + '] for help';
920 if (this.mode.has_input_prompt) {
921 help = 'enter /help for help';
923 terminal.write(0, this.window_width, 'MODE: ' + this.mode.short_desc + ' – ' + help);
925 draw_turn_line: function(n) {
926 terminal.write(1, this.window_width, 'TURN: ' + game.turn);
928 draw_history: function() {
929 let log_display_lines = [];
931 let y_offset_in_log = 0;
932 for (let line of this.log) {
933 let [new_lines, link_data] = this.msg_into_lines_of_width(line,
935 log_display_lines = log_display_lines.concat(new_lines);
936 for (const y in link_data) {
937 const rel_y = y_offset_in_log + parseInt(y);
938 log_links[rel_y] = [];
939 for (let link of link_data[y]) {
940 log_links[rel_y].push(link);
943 y_offset_in_log += new_lines.length;
945 let i = log_display_lines.length - 1;
946 for (let y = terminal.rows - 1 - this.height_input;
947 y >= this.height_header && i >= 0;
949 terminal.write(y, this.window_width, log_display_lines[i]);
951 for (const key of Object.keys(log_links)) {
952 if (parseInt(key) <= i) {
953 delete log_links[key];
956 let offset = [terminal.rows - this.height_input - log_display_lines.length,
958 this.offset_links(offset, log_links);
960 draw_info: function() {
961 let [lines, link_data] = this.msg_into_lines_of_width(explorer.get_info(),
963 let offset = [this.height_header, this.window_width];
964 for (let y = offset[0], i = 0; y < terminal.rows && i < lines.length; y++, i++) {
965 terminal.write(y, offset[1], lines[i]);
967 this.offset_links(offset, link_data);
969 draw_input: function() {
970 if (this.mode.has_input_prompt) {
971 for (let y = terminal.rows - this.height_input, i = 0; i < this.input_lines.length; y++, i++) {
972 terminal.write(y, this.window_width, this.input_lines[i]);
976 draw_help: function() {
977 let movement_keys_desc = '';
978 if (!this.mode.is_intro) {
979 movement_keys_desc = Object.keys(this.movement_keys).join(',');
981 let content = this.mode.short_desc + " help\n\n" + this.mode.help_intro + "\n\n";
982 if (this.mode.name == 'play') {
983 content += "Available actions:\n";
984 if (game.tasks.includes('MOVE')) {
985 content += "[" + movement_keys_desc + "] – move player\n";
987 if (game.tasks.includes('PICK_UP')) {
988 content += "[" + this.keys.take_thing + "] – pick up thing\n";
990 if (game.tasks.includes('DROP')) {
991 content += "[" + this.keys.drop_thing + "] – drop thing\n";
993 content += "[" + tui.keys.teleport + "] – teleport\n";
995 } else if (this.mode.name == 'study') {
996 content += "Available actions:\n";
997 content += '[' + movement_keys_desc + '] – move question mark\n';
998 content += '[' + this.keys.toggle_map_mode + '] – toggle map view\n';
1000 } else if (this.mode.name == 'edit') {
1001 content += "Available actions:\n";
1002 if (game.tasks.includes('MOVE')) {
1003 content += "[" + movement_keys_desc + "] – move player\n";
1005 if (game.tasks.includes('FLATTEN_SURROUNDINGS')) {
1006 content += "[" + tui.keys.flatten + "] – flatten surroundings\n";
1008 content += '[' + this.keys.toggle_map_mode + '] – toggle map view\n';
1010 } else if (this.mode.name == 'control_tile_draw') {
1011 content += "Available actions:\n";
1012 content += "[" + tui.keys.toggle_tile_draw + "] – toggle protection character drawing\n";
1014 } else if (this.mode.name == 'chat') {
1015 content += '/nick NAME – re-name yourself to NAME\n';
1016 content += '/' + this.keys.switch_to_play + ' or /play – switch to play mode\n';
1017 content += '/' + this.keys.switch_to_study + ' or /study – switch to study mode\n';
1018 content += '/' + this.keys.switch_to_edit + ' or /edit – switch to map edit mode\n';
1019 content += '/' + this.keys.switch_to_admin_enter + ' or /admin – switch to admin mode\n';
1021 content += this.mode.list_available_modes();
1023 if (!this.mode.has_input_prompt) {
1024 start_x = this.window_width
1026 terminal.drawBox(0, start_x, terminal.rows, this.window_width);
1027 let [lines, _] = this.msg_into_lines_of_width(content, this.window_width);
1028 for (let y = 0, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
1029 terminal.write(y, start_x, lines[i]);
1032 toggle_tile_draw: function() {
1033 if (tui.tile_draw) {
1034 tui.tile_draw = false;
1036 tui.tile_draw = true;
1039 toggle_map_mode: function() {
1040 if (tui.map_mode == 'terrain only') {
1041 tui.map_mode = 'terrain + annotations';
1042 } else if (tui.map_mode == 'terrain + annotations') {
1043 tui.map_mode = 'terrain + things';
1044 } else if (tui.map_mode == 'terrain + things') {
1045 tui.map_mode = 'protections';
1046 } else if (tui.map_mode == 'protections') {
1047 tui.map_mode = 'terrain only';
1050 full_refresh: function() {
1052 terminal.drawBox(0, 0, terminal.rows, terminal.cols);
1053 this.recalc_input_lines();
1054 if (this.mode.is_intro) {
1055 this.draw_history();
1058 if (game.turn_complete) {
1060 this.draw_turn_line();
1062 this.draw_mode_line();
1063 if (this.mode.shows_info) {
1066 this.draw_history();
1070 if (this.show_help) {
1082 this.map_control = "";
1083 this.map_size = [0,0];
1084 this.player_id = -1;
1088 get_thing: function(id_, create_if_not_found=false) {
1089 if (id_ in game.things) {
1090 return game.things[id_];
1091 } else if (create_if_not_found) {
1092 let t = new Thing([0,0]);
1093 game.things[id_] = t;
1097 move: function(start_position, direction) {
1098 let target = [start_position[0], start_position[1]];
1099 if (direction == 'LEFT') {
1101 } else if (direction == 'RIGHT') {
1103 } else if (game.map_geometry == 'Square') {
1104 if (direction == 'UP') {
1106 } else if (direction == 'DOWN') {
1109 } else if (game.map_geometry == 'Hex') {
1110 let start_indented = start_position[0] % 2;
1111 if (direction == 'UPLEFT') {
1113 if (!start_indented) {
1116 } else if (direction == 'UPRIGHT') {
1118 if (start_indented) {
1121 } else if (direction == 'DOWNLEFT') {
1123 if (!start_indented) {
1126 } else if (direction == 'DOWNRIGHT') {
1128 if (start_indented) {
1133 if (target[0] < 0 || target[1] < 0 ||
1134 target[0] >= this.map_size[0] || target[1] >= this.map_size[1]) {
1139 teleport: function() {
1140 let player = this.get_thing(game.player_id);
1141 if (player.position in this.portals) {
1142 server.reconnect_to(this.portals[player.position]);
1144 terminal.blink_screen();
1145 tui.log_msg('? not standing on portal')
1153 server.init(websocket_location);
1159 move: function(direction) {
1160 let target = game.move(this.position, direction);
1162 this.position = target
1163 if (tui.mode.shows_info) {
1165 } else if (tui.tile_draw) {
1166 this.send_tile_control_command();
1169 terminal.blink_screen();
1172 update_info_db: function(yx, str) {
1173 this.info_db[yx] = str;
1174 if (tui.mode.name == 'study') {
1178 empty_info_db: function() {
1180 this.info_hints = [];
1181 if (tui.mode.name == 'study') {
1185 query_info: function() {
1186 server.send(["GET_ANNOTATION", unparser.to_yx(explorer.position)]);
1188 get_info: function() {
1189 let info = "MAP VIEW: " + tui.map_mode + "\n";
1190 let position_i = this.position[0] * game.map_size[1] + this.position[1];
1191 if (game.fov[position_i] != '.') {
1192 return info + 'outside field of view';
1194 let terrain_char = game.map[position_i]
1195 let terrain_desc = '?'
1196 if (game.terrains[terrain_char]) {
1197 terrain_desc = game.terrains[terrain_char];
1199 info += 'TERRAIN: "' + terrain_char + '" / ' + terrain_desc + "\n";
1200 let protection = game.map_control[position_i];
1201 if (protection == '.') {
1202 protection = 'unprotected';
1204 info += 'PROTECTION: ' + protection + '\n';
1205 for (let t_id in game.things) {
1206 let t = game.things[t_id];
1207 if (t.position[0] == this.position[0] && t.position[1] == this.position[1]) {
1208 let symbol = game.thing_types[t.type_];
1209 info += "THING: " + t.type_ + " / " + symbol;
1210 if (t.player_char) {
1211 info += t.player_char;
1214 info += " (" + t.name_ + ")";
1219 if (this.position in game.portals) {
1220 info += "PORTAL: " + game.portals[this.position] + "\n";
1222 if (this.position in this.info_db) {
1223 info += "ANNOTATIONS: " + this.info_db[this.position];
1225 info += 'waiting …';
1229 annotate: function(msg) {
1230 if (msg.length == 0) {
1231 msg = " "; // triggers annotation deletion
1233 server.send(["ANNOTATE", unparser.to_yx(explorer.position), msg, tui.password]);
1235 set_portal: function(msg) {
1236 if (msg.length == 0) {
1237 msg = " "; // triggers portal deletion
1239 server.send(["PORTAL", unparser.to_yx(explorer.position), msg, tui.password]);
1241 send_tile_control_command: function() {
1242 server.send(["SET_TILE_CONTROL", unparser.to_yx(this.position), tui.tile_control_char]);
1246 tui.inputEl.addEventListener('input', (event) => {
1247 if (tui.mode.has_input_prompt) {
1248 let max_length = tui.window_width * terminal.rows - tui.input_prompt.length;
1249 if (tui.inputEl.value.length > max_length) {
1250 tui.inputEl.value = tui.inputEl.value.slice(0, max_length);
1252 } else if (tui.mode.name == 'write' && tui.inputEl.value.length > 0) {
1253 server.send(["TASK:WRITE", tui.inputEl.value[0], tui.password]);
1254 tui.switch_mode('edit');
1258 document.onclick = function() {
1259 tui.show_help = false;
1261 tui.inputEl.addEventListener('keydown', (event) => {
1262 tui.show_help = false;
1263 if (event.key == 'Enter') {
1264 event.preventDefault();
1266 if (tui.mode.has_input_prompt && event.key == 'Enter' && tui.inputEl.value == '/help') {
1267 tui.show_help = true;
1268 tui.inputEl.value = "";
1269 tui.restore_input_values();
1270 } else if (!tui.mode.has_input_prompt && event.key == tui.keys.help
1271 && !tui.mode.is_single_char_entry) {
1272 tui.show_help = true;
1273 } else if (tui.mode.name == 'login' && event.key == 'Enter') {
1274 tui.login_name = tui.inputEl.value;
1275 server.send(['LOGIN', tui.inputEl.value]);
1276 tui.inputEl.value = "";
1277 } else if (tui.mode.name == 'control_pw_pw' && event.key == 'Enter') {
1278 if (tui.inputEl.value.length == 0) {
1279 tui.log_msg('@ aborted');
1281 server.send(['SET_MAP_CONTROL_PASSWORD',
1282 tui.tile_control_char, tui.inputEl.value]);
1283 tui.log_msg('@ sent new password for protection character "' + tui.tile_control_char + '".');
1285 tui.switch_mode('admin');
1286 } else if (tui.mode.name == 'portal' && event.key == 'Enter') {
1287 explorer.set_portal(tui.inputEl.value);
1288 tui.switch_mode('edit');
1289 } else if (tui.mode.name == 'name_thing' && event.key == 'Enter') {
1290 if (tui.inputEl.value.length == 0) {
1291 tui.inputEl.value = " ";
1293 server.send(["THING_NAME", tui.selected_thing_id, tui.inputEl.value]);
1294 tui.switch_mode('edit');
1295 } else if (tui.mode.name == 'annotate' && event.key == 'Enter') {
1296 explorer.annotate(tui.inputEl.value);
1297 tui.switch_mode('edit');
1298 } else if (tui.mode.name == 'password' && event.key == 'Enter') {
1299 if (tui.inputEl.value.length == 0) {
1300 tui.inputEl.value = " ";
1302 tui.password = tui.inputEl.value
1303 tui.switch_mode('edit');
1304 } else if (tui.mode.name == 'admin_enter' && event.key == 'Enter') {
1305 server.send(['BECOME_ADMIN', tui.inputEl.value]);
1306 tui.switch_mode('play');
1307 } else if (tui.mode.name == 'control_pw_type' && event.key == 'Enter') {
1308 if (tui.inputEl.value.length != 1) {
1309 tui.log_msg('@ entered non-single-char, therefore aborted');
1310 tui.switch_mode('admin');
1312 tui.tile_control_char = tui.inputEl.value[0];
1313 tui.switch_mode('control_pw_pw');
1315 } else if (tui.mode.name == 'control_tile_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_tile_draw');
1323 } else if (tui.mode.name == 'chat' && event.key == 'Enter') {
1324 let tokens = parser.tokenize(tui.inputEl.value);
1325 if (tokens.length > 0 && tokens[0].length > 0) {
1326 if (tui.inputEl.value[0][0] == '/') {
1327 if (tokens[0].slice(1) == 'play' || tokens[0][1] == tui.keys.switch_to_play) {
1328 tui.switch_mode('play');
1329 } else if (tokens[0].slice(1) == 'study' || tokens[0][1] == tui.keys.switch_to_study) {
1330 tui.switch_mode('study');
1331 } else if (tokens[0].slice(1) == 'edit' || tokens[0][1] == tui.keys.switch_to_edit) {
1332 tui.switch_mode('edit');
1333 } else if (tokens[0].slice(1) == 'admin' || tokens[0][1] == tui.keys.switch_to_admin_enter) {
1334 tui.switch_mode('admin_enter');
1335 } else if (tokens[0].slice(1) == 'nick') {
1336 if (tokens.length > 1) {
1337 server.send(['NICK', tokens[1]]);
1339 tui.log_msg('? need new name');
1342 tui.log_msg('? unknown command');
1345 server.send(['ALL', tui.inputEl.value]);
1347 } else if (tui.inputEl.valuelength > 0) {
1348 server.send(['ALL', tui.inputEl.value]);
1350 tui.inputEl.value = "";
1351 } else if (tui.mode.name == 'play') {
1352 if (tui.mode.mode_switch_on_key(event)) {
1354 } else if (event.key === tui.keys.take_thing
1355 && game.tasks.includes('PICK_UP')) {
1356 server.send(["TASK:PICK_UP"]);
1357 } else if (event.key === tui.keys.drop_thing
1358 && game.tasks.includes('DROP')) {
1359 server.send(["TASK:DROP"]);
1360 } else if (event.key in tui.movement_keys
1361 && game.tasks.includes('MOVE')) {
1362 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1363 } else if (event.key === tui.keys.teleport) {
1366 } else if (tui.mode.name == 'study') {
1367 if (tui.mode.mode_switch_on_key(event)) {
1369 } else if (event.key in tui.movement_keys) {
1370 explorer.move(tui.movement_keys[event.key]);
1371 } else if (event.key == tui.keys.toggle_map_mode) {
1372 tui.toggle_map_mode();
1374 } else if (tui.mode.name == 'control_tile_draw') {
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_tile_draw) {
1380 tui.toggle_tile_draw();
1382 } else if (tui.mode.name == 'admin') {
1383 if (tui.mode.mode_switch_on_key(event)) {
1386 } else if (tui.mode.name == 'edit') {
1387 if (tui.mode.mode_switch_on_key(event)) {
1389 } else if (event.key in tui.movement_keys
1390 && game.tasks.includes('MOVE')) {
1391 server.send(['TASK:MOVE', tui.movement_keys[event.key]]);
1392 } else if (event.key === tui.keys.flatten
1393 && game.tasks.includes('FLATTEN_SURROUNDINGS')) {
1394 server.send(["TASK:FLATTEN_SURROUNDINGS", tui.password]);
1395 } else if (event.key == tui.keys.toggle_map_mode) {
1396 tui.toggle_map_mode();
1402 rows_selector.addEventListener('input', function() {
1403 if (rows_selector.value % 4 != 0 || rows_selector.value < 24) {
1406 window.localStorage.setItem(rows_selector.id, rows_selector.value);
1407 terminal.initialize();
1410 cols_selector.addEventListener('input', function() {
1411 if (cols_selector.value % 4 != 0 || cols_selector.value < 80) {
1414 window.localStorage.setItem(cols_selector.id, cols_selector.value);
1415 terminal.initialize();
1416 tui.window_width = terminal.cols / 2,
1419 for (let key_selector of key_selectors) {
1420 key_selector.addEventListener('input', function() {
1421 window.localStorage.setItem(key_selector.id, key_selector.value);
1425 window.setInterval(function() {
1426 if (server.connected) {
1427 server.send(['PING']);
1429 server.reconnect_to(server.url);
1430 tui.log_msg('@ attempting reconnect …')
1433 window.setInterval(function() {
1435 if (document.activeElement == tui.inputEl) {
1436 val = "on (click outside terminal to change)";
1438 val = "off (click into terminal to change)";
1440 document.getElementById("keyboard_control").textContent = val;
1442 document.getElementById("terminal").onclick = function() {
1443 tui.inputEl.focus();
1445 document.getElementById("help").onclick = function() {
1446 tui.show_help = true;
1449 for (const switchEl of document.querySelectorAll('[id^="switch_to_"]')) {
1450 const mode = switchEl.id.slice("switch_to_".length);
1451 switchEl.onclick = function() {
1452 tui.switch_mode(mode);
1456 document.getElementById("toggle_tile_draw").onclick = function() {
1457 tui.toggle_tile_draw();
1459 document.getElementById("toggle_map_mode").onclick = function() {
1460 tui.toggle_map_mode();
1463 document.getElementById("take_thing").onclick = function() {
1464 server.send(['TASK:PICK_UP']);
1466 document.getElementById("drop_thing").onclick = function() {
1467 server.send(['TASK:DROP']);
1469 document.getElementById("flatten").onclick = function() {
1470 server.send(['TASK:FLATTEN_SURROUNDINGS', tui.password]);
1472 document.getElementById("teleport").onclick = function() {
1475 document.getElementById("move_upleft").onclick = function() {
1476 if (tui.mode.name == 'play' || tui.mode.name == 'edit') {
1477 server.send(['TASK:MOVE', 'UPLEFT']);
1479 explorer.move('UPLEFT');
1482 document.getElementById("move_left").onclick = function() {
1483 if (tui.mode.name == 'play' || tui.mode.name == 'edit') {
1484 server.send(['TASK:MOVE', 'LEFT']);
1486 explorer.move('LEFT');
1489 document.getElementById("move_downleft").onclick = function() {
1490 if (tui.mode.name == 'play' || tui.mode.name == 'edit') {
1491 server.send(['TASK:MOVE', 'DOWNLEFT']);
1493 explorer.move('DOWNLEFT');
1496 document.getElementById("move_down").onclick = function() {
1497 if (tui.mode.name == 'play' || tui.mode.name == 'edit') {
1498 server.send(['TASK:MOVE', 'DOWN']);
1500 explorer.move('DOWN');
1503 document.getElementById("move_up").onclick = function() {
1504 if (tui.mode.name == 'play' || tui.mode.name == 'edit') {
1505 server.send(['TASK:MOVE', 'UP']);
1507 explorer.move('UP');
1510 document.getElementById("move_upright").onclick = function() {
1511 if (tui.mode.name == 'play' || tui.mode.name == 'edit') {
1512 server.send(['TASK:MOVE', 'UPRIGHT']);
1514 explorer.move('UPRIGHT');
1517 document.getElementById("move_right").onclick = function() {
1518 if (tui.mode.name == 'play' || tui.mode.name == 'edit') {
1519 server.send(['TASK:MOVE', 'RIGHT']);
1521 explorer.move('RIGHT');
1524 document.getElementById("move_downright").onclick = function() {
1525 if (tui.mode.name == 'play' || tui.mode.name == 'edit') {
1526 server.send(['TASK:MOVE', 'DOWNRIGHT']);
1528 explorer.move('DOWNRIGHT');