home · contact · privacy
Add clickable URL links to web client.
[plomrogue2] / rogue_chat_nocanvas_monochrome.html
index 7928e2412f8b6c2acfd3006dad58bb0c164a0e95..9db72e4f8ca6374db566469f509837ca192a1bb2 100644 (file)
@@ -32,6 +32,7 @@ terminal columns: <input id="n_cols" type="number" step=4 min=80 value=80 />
 <button id="toggle_map_mode">toggle terrain/annotations/control view</button>
 <button id="switch_to_admin">become admin</button>
 <button id="switch_to_control_pw_type">change tile control password</button>
+<button id="switch_to_control_tile_type">change tiles control</button>
 </div>
 <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 />
 <ul>
@@ -57,6 +58,7 @@ terminal columns: <input id="n_cols" type="number" step=4 min=80 value=80 />
 <li>enter tile password (from play mode): <input id="key_switch_to_password" type="text" value="P" />
 <li>enter admin password (from play mode): <input id="key_switch_to_admin" type="text" value="A" />
 <li>change tile control password (from play mode): <input id="key_switch_to_control_pw_type" type="text" value="C" />
+<li>change tiles control (from play mode): <input id="key_switch_to_control_tile_type" type="text" value="Q" />
 <li>annotate tile (from play mode): <input id="key_switch_to_annotate" type="text" value="M" />
 <li>annotate portal (from play mode): <input id="key_switch_to_portal" type="text" value="T" />
 <li>toggle terrain/annotations/control view (from study mode): <input id="key_toggle_map_mode" type="text" value="M" />
@@ -84,9 +86,17 @@ let mode_helps = {
         'long': 'This mode is the first of two steps to change the password for a tile control character.  First enter the tile control character for which you want to change the password!'
     },
     'control_pw_pw': {
-        'short': '',
+        'short': 'change tile control password',
         'long': 'This mode is the second of two steps to change the password for a tile control character.  Enter the new password for the tile control character you chose.'
     },
+    'control_tile_type': {
+        'short': 'change tiles control',
+        'long': 'This mode is the first of two steps to change tile control areas on the map.  First enter the tile control character you want to write.'
+    },
+    'control_tile_draw': {
+        'short': 'change tiles control',
+        'long': 'This mode is the second of two steps to change tile control areas on the map.  Move cursor around the map to draw selected tile control character'
+    },
     'annotate': {
         'short': 'annotation',
         '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.'
@@ -100,19 +110,19 @@ let mode_helps = {
         '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:'
     },
     'login': {
-        'short': '',
+        'short': 'login',
         'long': 'Pick your player name.'
     },
     'waiting_for_server': {
-        'short': '',
+        'short': 'waiting for server response',
         'long': 'Waiting for a server response.'
     },
     'post_login_wait': {
-        'short': '',
+        'short': 'waiting for server response',
         'long': 'Waiting for a server response.'
     },
     'password': {
-        'short': 'password input',
+        'short': 'map edit password',
         '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.'
     },
     'admin': {
@@ -170,12 +180,39 @@ let terminal = {
       }, 100);
   },
   refresh: function() {
-      let pre_string = '';
+      function escapeHTML(str) {
+          return str.
+              replace(/&/g, '&amp;').
+              replace(/</g, '&lt;').
+              replace(/>/g, '&gt;').
+              replace(/'/g, '&apos;').
+              replace(/"/g, '&quot;');
+      };
+      let pre_content = '';
       for (let y = 0; y < this.rows; y++) {
           let line = this.content[y].join('');
-          pre_string += line + '\n';
+          let chunks = [];
+          if (y in tui.links) {
+              let start_x = 0;
+              for (let span of tui.links[y]) {
+                  chunks.push(escapeHTML(line.slice(start_x, span[0])));
+                  chunks.push('<a href="');
+                  chunks.push(escapeHTML(span[2]));
+                  chunks.push('">');
+                  chunks.push(escapeHTML(line.slice(span[0], span[1])));
+                  chunks.push('</a>');
+                  start_x = span[1];
+              }
+              chunks.push(escapeHTML(line.slice(start_x)));
+          } else {
+              chunks = [escapeHTML(line)];
+          }
+          for (const chunk of chunks) {
+              pre_content += chunk;
+          }
+          pre_content += '\n';
       }
-      this.pre_el.textContent = pre_string;
+      this.pre_el.innerHTML = pre_content;
   },
   write: function(start_y, start_x, msg) {
       for (let x = start_x, i = 0; x < this.cols && i < msg.length; x++, i++) {
@@ -428,6 +465,7 @@ class Mode {
     }
 }
 let tui = {
+  links: {},
   log: [],
   input_prompt: '> ',
   input_lines: [],
@@ -452,12 +490,17 @@ let tui = {
   mode_password: new Mode('password', true),
   mode_admin: new Mode('admin', true),
   mode_control_pw_pw: new Mode('control_pw_pw', true),
+  mode_control_tile_type: new Mode('control_tile_type',
+                                   false, false, false, true),
+  mode_control_tile_draw: new Mode('control_tile_draw'),
   init: function() {
       this.mode_play.available_modes = ["chat", "study", "edit",
                                         "annotate", "portal",
                                         "password", "admin",
-                                        "control_pw_type"]
+                                        "control_pw_type",
+                                        "control_tile_type"]
       this.mode_study.available_modes = ["chat", "play"]
+      this.mode_control_tile_draw.available_modes = ["play"]
       this.mode = this.mode_waiting_for_server;
       this.inputEl = document.getElementById("input");
       this.inputEl.focus();
@@ -492,9 +535,14 @@ let tui = {
     this.inputEl.focus();
     this.map_mode = 'terrain';
     this.mode = this['mode_' + mode_name];
-    if (this.mode.shows_info && game.player_id in game.things) {
-      explorer.position = game.things[game.player_id].position;
-      explorer.query_info();
+    if (game.player_id in game.things && (this.mode.shows_info || this.mode.name == 'control_tile_draw')) {
+        explorer.position = game.things[game.player_id].position;
+        if (this.mode.shows_info) {
+        explorer.query_info();
+        } else if (this.mode.name == 'control_tile_draw') {
+            explorer.send_tile_control_command();
+            this.map_mode = 'control';
+        }
     }
     this.empty_input();
     this.restore_input_values();
@@ -512,6 +560,7 @@ let tui = {
     document.getElementById("switch_to_password").disabled = true;
     document.getElementById("switch_to_admin").disabled = true;
     document.getElementById("switch_to_control_pw_type").disabled = true;
+    document.getElementById("switch_to_control_tile_type").disabled = true;
     document.getElementById("move_left").disabled = true;
     document.getElementById("move_upleft").disabled = true;
     document.getElementById("move_up").disabled = true;
@@ -520,7 +569,7 @@ let tui = {
     document.getElementById("move_down").disabled = true;
     document.getElementById("move_downright").disabled = true;
     document.getElementById("move_right").disabled = true;
-    if (this.mode.name == 'play' || this.mode.name == 'study') {
+    if (this.mode.name == 'play' || this.mode.name == 'study' || this.mode.name == 'control_tile_draw') {
         document.getElementById("move_left").disabled = false;
         document.getElementById("move_right").disabled = false;
         if (game.map_geometry == 'Hex') {
@@ -567,6 +616,7 @@ let tui = {
         document.getElementById("switch_to_password").disabled = false;
         document.getElementById("switch_to_admin").disabled = false;
         document.getElementById("switch_to_control_pw_type").disabled = false;
+        document.getElementById("switch_to_control_tile_type").disabled = false;
     } else if (this.mode.name == 'study') {
         document.getElementById("toggle_map_mode").disabled = false;
     } else if (this.mode.is_single_char_entry) {
@@ -575,9 +625,23 @@ let tui = {
         this.log_msg('@ enter admin password:')
     } else if (this.mode.name == 'control_pw_pw') {
         this.log_msg('@ enter tile control password for "' + this.tile_control_char + '":');
+    } else if (this.mode.name == 'control_pw_pw') {
+        this.log_msg('@ enter tile control password for "' + this.tile_control_char + '":');
     }
     this.full_refresh();
   },
+  offset_links: function(offset, links) {
+      for (let y in links) {
+          let real_y = offset[0] + parseInt(y);
+          if (!this.links[real_y]) {
+              this.links[real_y] = [];
+          }
+          for (let link of links[y]) {
+              const offset_link = [link[0] + offset[1], link[1] + offset[1], link[2]];
+              this.links[real_y].push(offset_link);
+          }
+      }
+  },
   restore_input_values: function() {
       if (this.mode.name == 'annotate' && explorer.position in explorer.info_db) {
           let info = explorer.info_db[explorer.position];
@@ -603,24 +667,64 @@ let tui = {
       }
   },
   recalc_input_lines: function() {
-      this.input_lines = this.msg_into_lines_of_width(this.input_prompt + this.inputEl.value, this.window_width);
+      let _ = null;
+      [this.input_lines, _] = this.msg_into_lines_of_width(this.input_prompt + this.inputEl.value, this.window_width);
       this.height_input = this.input_lines.length;
   },
   msg_into_lines_of_width: function(msg, width) {
-    let chunk = "";
-    let lines = [];
-    for (let i = 0, x = 0; i < msg.length; i++, x++) {
-      if (x >= width || msg[i] == "\n") {
-        lines.push(chunk);
-        chunk = "";
-        x = 0;
+      function push_inner_link(y, end_x) {
+          if (!inner_links[y]) {
+              inner_links[y] = [];
+          };
+          inner_links[y].push([url_start_x, end_x, url]);
       };
-      if (msg[i] != "\n") {
-        chunk += msg[i];
+      const matches = msg.matchAll(/https?:\/\/[^\s]+/g)
+      let link_data = {};
+      let url_ends = [];
+      for (const match of matches) {
+          const url = match[0];
+          const url_start = match.index;
+          const url_end = match.index + match[0].length;
+          link_data[url_start] = url;
+          url_ends.push(url_end);
       }
-    }
-    lines.push(chunk);
-    return lines;
+      let url_start_x = 0;
+      let url = '';
+      let inner_links = {};
+      let in_link = false;
+      let chunk = "";
+      let lines = [];
+      for (let i = 0, x = 0, y = 0; i < msg.length; i++, x++) {
+          if (x >= width || msg[i] == "\n") {
+              if (in_link) {
+                  push_inner_link(y, chunk.length);
+                  url_start_x = 0;
+              };
+              lines.push(chunk);
+              chunk = "";
+              x = 0;
+              if (msg[i] == "\n") {
+                  x -= 1;
+              };
+              y += 1;
+          };
+          if (msg[i] != "\n") {
+              chunk += msg[i];
+          };
+          if (i in link_data) {
+              url_start_x = x;
+              url = link_data[i];
+              in_link = true;
+          } else if (url_ends.includes(i)) {
+              push_inner_link(y, x);
+              in_link = false;
+          }
+      }
+      lines.push(chunk);
+      if (in_link) {
+          push_inner_link(lines.length - 1, chunk.length);
+      }
+      return [lines, inner_links];
   },
   log_msg: function(msg) {
       this.log.push(msg);
@@ -669,7 +773,7 @@ let tui = {
             used_positions.push(t.position.toString());
         };
     }
-    if (tui.mode.shows_info) {
+    if (tui.mode.shows_info || tui.mode.name == 'control_tile_draw') {
         map_lines_split[explorer.position[0]][explorer.position[1]] = '??';
     }
     let map_lines = []
@@ -714,28 +818,51 @@ let tui = {
       if (this.mode.has_input_prompt) {
           help = 'enter /help for help';
       }
-      terminal.write(0, this.window_width, 'MODE: ' + this.mode.name + ' – ' + help);
+      terminal.write(0, this.window_width, 'MODE: ' + this.mode.short_desc + ' – ' + help);
   },
   draw_turn_line: function(n) {
     terminal.write(1, this.window_width, 'TURN: ' + game.turn);
   },
   draw_history: function() {
       let log_display_lines = [];
+      let log_links = {};
+      let y_offset_in_log = 0;
       for (let line of this.log) {
-          log_display_lines = log_display_lines.concat(this.msg_into_lines_of_width(line, this.window_width));
+          let [new_lines, link_data] = this.msg_into_lines_of_width(line,
+                                                                    this.window_width)
+          log_display_lines = log_display_lines.concat(new_lines);
+          for (const y in link_data) {
+              const rel_y = y_offset_in_log + parseInt(y);
+              log_links[rel_y] = [];
+              for (let link of link_data[y]) {
+                  log_links[rel_y].push(link);
+              }
+          }
+          y_offset_in_log += new_lines.length;
       };
-      for (let y = terminal.rows - 1 - this.height_input,
-               i = log_display_lines.length - 1;
+      let i = log_display_lines.length - 1;
+      for (let y = terminal.rows - 1 - this.height_input;
            y >= this.height_header && i >= 0;
            y--, i--) {
           terminal.write(y, this.window_width, log_display_lines[i]);
       }
+      for (const key of Object.keys(log_links)) {
+          if (parseInt(key) <= i) {
+              delete log_links[key];
+          }
+      }
+      let offset = [terminal.rows - this.height_input - log_display_lines.length,
+                    this.window_width];
+      this.offset_links(offset, log_links);
   },
   draw_info: function() {
-    let lines = this.msg_into_lines_of_width(explorer.get_info(), this.window_width);
-    for (let y = this.height_header, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
-      terminal.write(y, this.window_width, lines[i]);
-    }
+      let [lines, link_data] = this.msg_into_lines_of_width(explorer.get_info(),
+                                                            this.window_width);
+      let offset = [this.height_header, this.window_width];
+      for (let y = offset[0], i = 0; y < terminal.rows && i < lines.length; y++, i++) {
+        terminal.write(y, offset[1], lines[i]);
+      }
+      this.offset_links(offset, link_data);
   },
   draw_input: function() {
     if (this.mode.has_input_prompt) {
@@ -746,7 +873,7 @@ let tui = {
   },
   draw_help: function() {
       let movement_keys_desc = Object.keys(this.movement_keys).join(',');
-      let content = this.mode.name + " mode help\n\n" + this.mode.help_intro + "\n\n";
+      let content = this.mode.short_desc + " help\n\n" + this.mode.help_intro + "\n\n";
       if (this.mode.name == 'play') {
           content += "Available actions:\n";
           if (game.tasks.includes('MOVE')) {
@@ -779,12 +906,13 @@ let tui = {
           start_x = this.window_width
       }
       terminal.drawBox(0, start_x, terminal.rows, this.window_width);
-      let lines = this.msg_into_lines_of_width(content, this.window_width);
+      let [lines, _] = this.msg_into_lines_of_width(content, this.window_width);
       for (let y = 0, i = 0; y < terminal.rows && i < lines.length; y++, i++) {
           terminal.write(y, start_x, lines[i]);
       }
   },
   full_refresh: function() {
+    this.links = {};
     terminal.drawBox(0, 0, terminal.rows, terminal.cols);
     if (this.mode.is_intro) {
         this.draw_history();
@@ -895,7 +1023,11 @@ let explorer = {
         let target = game.move(this.position, direction);
         if (target) {
             this.position = target
-            this.query_info();
+            if (tui.mode.shows_info) {
+                this.query_info();
+            } else if (tui.mode.name == 'control_tile_draw') {
+                this.send_tile_control_command();
+            }
         } else {
             terminal.blink_screen();
         };
@@ -968,6 +1100,9 @@ let explorer = {
             msg = " ";  // triggers portal deletion
         }
         server.send(["PORTAL", unparser.to_yx(explorer.position), msg, tui.password]);
+    },
+    send_tile_control_command: function() {
+        server.send(["SET_TILE_CONTROL", unparser.to_yx(this.position), tui.tile_control_char]);
     }
 }
 
@@ -984,6 +1119,9 @@ tui.inputEl.addEventListener('input', (event) => {
     } else if (tui.mode.name == 'control_pw_type' && tui.inputEl.value.length > 0) {
         tui.tile_control_char = tui.inputEl.value[0];
         tui.switch_mode('control_pw_pw');
+    } else if (tui.mode.name == 'control_tile_type' && tui.inputEl.value.length > 0) {
+        tui.tile_control_char = tui.inputEl.value[0];
+        tui.switch_mode('control_tile_draw');
     }
     tui.full_refresh();
 }, false);
@@ -1077,8 +1215,6 @@ tui.inputEl.addEventListener('keydown', (event) => {
     } else if (tui.mode.name == 'study') {
         if (tui.mode.mode_switch_on_key(event)) {
               null;
-        } else if (event.key == tui.keys.switch_to_play) {
-            tui.switch_mode('play');
         } else if (event.key in tui.movement_keys) {
             explorer.move(tui.movement_keys[event.key]);
         } else if (event.key == tui.keys.toggle_map_mode) {
@@ -1090,6 +1226,12 @@ tui.inputEl.addEventListener('keydown', (event) => {
                 tui.map_mode = 'terrain';
             }
         };
+    } else if (tui.mode.name == 'control_tile_draw') {
+        if (tui.mode.mode_switch_on_key(event)) {
+              null;
+        } else if (event.key in tui.movement_keys) {
+            explorer.move(tui.movement_keys[event.key]);
+        };
     }
     tui.full_refresh();
 }, false);
@@ -1168,6 +1310,10 @@ document.getElementById("switch_to_control_pw_type").onclick = function() {
     tui.switch_mode('control_pw_type');
     tui.full_refresh();
 };
+document.getElementById("switch_to_control_tile_type").onclick = function() {
+    tui.switch_mode('control_tile_type');
+    tui.full_refresh();
+};
 document.getElementById("toggle_map_mode").onclick = function() {
     if (tui.map_mode == 'terrain') {
         tui.map_mode = 'annotations';