view rlgterm.js @ 76:a497ecd116d9

Improvements to the keyboard. Add a number pad to the keyboard. Make it hidden by default for RLG-Web.
author John "Elwin" Edwards <elwin@sdf.org>
date Sat, 23 Jun 2012 17:11:51 -0700
parents 2984604ce3e6
children f8bb37f48d58
line wrap: on
line source

/* rlgterm.js: Roguelike Gallery's driver for termemu.js */

// A state machine that keeps track of polling the server.
var ajaxstate = {
  state: 0,
  timerID: null,
  clear: function () {
    if (this.timerID != null) {
      window.clearTimeout(this.timerID);
      this.timerID = null;
    }
  },
  set: function (ms) {
    this.clear();
    this.timerID = window.setTimeout(getData, ms);
  },
  gotdata: function () {
    this.set(1000);
    this.state = 1;
  },
  gotnothing: function () {
    if (this.state == 0) {
      this.set(1000);
      this.state = 1;
    }
    else if (this.state < 4) {
      this.set(4000);
      this.state++;
    }
    else if (session.playing) {
      if (this.state < 8) {
        this.set(15000);
        this.state++;
      }
      else {
        /* It's been over a minute.  Stop polling. */
        this.clear();
      }
    }
    else {
      /* If watching, it can't stop polling entirely, because there
       * are no POST events to start it up again. */
      this.set(10000);
    }
  },
  posted: function (wasdata) {
    if (wasdata) {
      this.set(1000);
      this.state = 1;
    }
    else {
      this.set(200);
      this.state = 0;
    }
  }
};

/* Data on the available games. */
var games = {
  "rogue3": {
    "name": "Rogue V3",
    "uname": "rogue3"
  },
  "rogue4": {
    "name": "Rogue V4",
    "uname": "rogue4"
  },
  "rogue5": {
    "name": "Rogue V5",
    "uname": "rogue5"
  },
  "srogue": {
    "name": "Super-Rogue",
    "uname": "srogue"
  }
};

var session = {
  /* The session id assigned by the server. */
  id: null,
  /* Login name and key */
  lname: null,
  lcred: null,
  /* Whether the game is being played or just watched. */
  playing: false
};

/* The interval ID for checking the status of current games. */
var statInterval = null;
/* How frequently to check. */
var statDelta = 8000;

function writeData(hexstr) {
  var codenum;
  var codes = [];
  var nc;
  var u8wait = 0; /* Stores bits from previous bytes of multibyte sequences. */
  var expect = 0; /* The number of 10------ bytes expected. */
  /* UTF-8 translation. */
  for (var i = 0; i < hexstr.length; i += 2) {
    nc = Number("0x" + hexstr.substr(i, 2));
    if (nc < 0x7F) {
      /* 0------- */
      codes.push(nc);
      /* Any incomplete sequence will be discarded. */
      u8wait = 0;
      expect = 0;
    }
    else if (nc < 0xC0) {
      /* 10------ : part of a multibyte sequence */
      if (expect > 0) {
        u8wait <<= 6;
        u8wait += (nc & 0x3F);
        expect--;
        if (expect == 0) {
          codes.push(u8wait);
          u8wait = 0;
        }
      }
      else {
        /* Assume an initial byte was missed. */
        u8wait = 0;
      }
    }
    /* These will all discard any incomplete sequence. */
    else if (nc < 0xE0) {
      /* 110----- : introduces 2-byte sequence */
      u8wait = (nc & 0x1F);
      expect = 1;
    }
    else if (nc < 0xF0) {
      /* 1110---- : introduces 3-byte sequence */
      u8wait = (nc & 0x0F);
      expect = 2;
    }
    else if (nc < 0xF8) {
      /* 11110--- : introduces 4-byte sequence */
      u8wait = (nc & 0x07);
      expect = 3;
    }
    else if (nc < 0xFC) {
      /* 111110-- : introduces 5-byte sequence */
      u8wait = (nc & 0x03);
      expect = 4;
    }
    else if (nc < 0xFE) {
      /* 1111110- : introduces 6-byte sequence */
      u8wait = (nc & 0x01);
      expect = 5;
    }
    else {
      /* 1111111- : should never appear */
      u8wait = 0;
      expect = 0;
    }
    /* Supporting all 31 bits is probably overkill... */
  }
  termemu.write(codes);
  return;
}

/* State for sending and receiving messages. */
var nsend = 0; // The number of the next packet to send.
var nrecv = 0; // The next packet expected.
var msgQ = []; // Queue for out-of-order messages.

/* Processes a message from the server, returning true or false if it was a 
 * data message with or without data, null if not data.
 * All non-special responseTexts should be handed directly to this function.
 */
function processMsg(msg) {
  var msgDicts;
  var havedata = null; // eventual return value
  try {
    msgDicts = JSON.parse(msg);
  } catch (e) {
    if (e instanceof SyntaxError)
      return null;
  }
  if (msgDicts.length === 0)
    return false;
  for (var j = 0; j < msgDicts.length; j++) {
    if (!msgDicts[j].t)
      continue;
    else if (msgDicts[j].t == "E") {
      if (msgDicts[j].c == 1 || msgDicts[j].c == 6 || msgDicts[j].c == 7) {
        gameover();
        if (msgDicts[j].c == 1) {
          logout();
        }
      }
      debug(1, "Server error: " + msgDicts[j].s);
    }
    // A data message
    else if (msgDicts[j].t == "d") {
      if (msgDicts[j].n === nrecv) {
        writeData(msgDicts[j].d);
        nrecv++;
        /* Process anything in the queue that's now ready. */
        var next;
        while ((next = msgQ.shift()) !== undefined) {
          writeData(next.d);
          nrecv++;
        }
      }
      else if (msgDicts[j].n > nrecv) {
        /* The current message comes after one still missing.  Queue this one
         * for later use. */
        debug(1, "Got packet " + msgDicts[j].n + ", expected " + nrecv);
        msgQ[msgDicts[j].n - nrecv - 1] = msgDicts[j];
      }
      else {
        /* This message's number was encountered previously. */
        debug(1, "Discarding packet " + msgDicts[j].n + ", expected " + nrecv);
      }
      havedata = true;
    }
    else if (msgDicts[j].t == "T") {
      setTitle(msgDicts[j].d);
    }
    else if (msgDicts[j].t == "q") {
      gameover();
    }
    else {
      debug(1, "Unrecognized server message " + msg);
    }
  }
  return havedata;
}

function getData() {
  if (session.id == null)
    return;
  var datareq = new XMLHttpRequest();
  var msg = JSON.stringify({"id": session.id, "t": "n"});
  datareq.onerror = errHandler;
  datareq.onreadystatechange = function () {
    if (datareq.readyState == 4 && datareq.status == 200) {
      var wasdata = processMsg(datareq.responseText);
      if (wasdata != null) {
        if (wasdata)
          ajaxstate.gotdata();
        else
          ajaxstate.gotnothing();
      }
      return;
    }
  };
  datareq.open('POST', '/feed', true);
  datareq.send(msg);
  return;
}

function postResponseHandler() {
  if (this.readyState == 4 && this.status == 200) {
    // We might want to do something with wasdata someday.
    var wasdata = processMsg(this.responseText);
    ajaxstate.posted();
    return;
  }
}

function errHandler() {
  debug(1, "Server unavailable?");
}

function sendback(str) {
  /* For responding to terminal queries. */
  var msgDict = {"id": session.id, "t": "d", "n": nsend++, "d": str};
  var datareq = new XMLHttpRequest();
  datareq.onerror = errHandler;
  datareq.onreadystatechange = postResponseHandler;
  datareq.open('POST', '/feed', true);
  datareq.send(JSON.stringify(msgDict));
  return;
}

function sendkey(ev) {
  if (!session.playing)
    return;
  var keynum = ev.keyCode;
  var code;
  if (keynum >= 65 && keynum <= 90) {
    /* Letters. */
    if (ev.ctrlKey)
      keynum -= 64;
    else if (!ev.shiftKey)
      keynum += 32;
    code = keynum.toString(16);
    if (code.length < 2)
      code = "0" + code;
  }
  else if (keynum >= 48 && keynum <= 57) {
    /* The number row, NOT the numpad. */
    if (ev.shiftKey) {
      code = numShifts[keynum - 48].toString(16);
    }
    else {
      code = keynum.toString(16);
    }
  }
  else if (keynum in keyHexCodes) {
    if (ev.shiftKey)
      code = keyHexCodes[keynum][1];
    else
      code = keyHexCodes[keynum][0];
  }
  else if (keynum >= 16 && keynum <= 20) {
    /* Shift, Cntl, Alt, CAPSLOCK */
    return;
  }
  else {
    debug(1, "Ignoring keycode " + keynum);
    return;
  }
  ev.preventDefault();
  var datareq = new XMLHttpRequest();
  var msgDict = {"id": session.id, "t": "d", "n": nsend++, "d": code};
  datareq.onerror = errHandler;
  datareq.onreadystatechange = postResponseHandler;
  datareq.open('POST', '/feed', true);
  datareq.send(JSON.stringify(msgDict));
  return;
}

var charshifts = { '-': "5f", '=': "2b", '[': "7b", ']': "7d", '\\': "7c",
  ';': "3a", '\'': "22", ',': "3c", '.': "3e", '/': "3f", '`': "7e"
}

var kpkeys = { "KP1": "1b4f46", "KP2": "1b4f42", "KP3": "1b5b367e", 
               "KP4": "1b4f44", "KP5": "1b5b45", "KP6": "1b4f43", 
               "KP7": "1b4f48", "KP8": "1b4f41", "KP9": "1b5b357e" };

function vkey(c) {
  if (!session.playing)
    return;
  var keystr;
  if (c.match(/^[a-z]$/)) {
    if (termemu.ctrlp()) {
      var n = c.charCodeAt(0) - 96;
      keystr = n.toString(16);
      if (keystr.length < 2)
        keystr = "0" + keystr;
    }
    else if (termemu.shiftp())
      keystr = c.toUpperCase().charCodeAt(0).toString(16);
    else
      keystr = c.charCodeAt(0).toString(16);
  }
  else if (c.match(/^[0-9]$/)) {
    if (termemu.shiftp())
      keystr = numShifts[c.charCodeAt(0) - 48].toString(16);
    else
      keystr = c.charCodeAt(0).toString(16);
  }
  else if (c == '\n')
    keystr = "0a";
  else if (c == '\t')
    keystr = "09";
  else if (c == '\b')
    keystr = "08";
  else if (c == ' ')
    keystr = "20";
  else if (c in charshifts) {
    if (termemu.shiftp())
      keystr = charshifts[c];
    else
      keystr = c.charCodeAt(0).toString(16);
  }
  else if (c in kpkeys) {
    keystr = kpkeys[c];
  }
  else
    return;
  var datareq = new XMLHttpRequest();
  var msgDict = {"id": session.id, "t": "d", "n": nsend++, "d": keystr};
  datareq.onerror = errHandler;
  datareq.onreadystatechange = postResponseHandler;
  datareq.open('POST', '/feed', true);
  datareq.send(JSON.stringify(msgDict));
  return;
}

function setup() {
  keyHexCodes.init();
  termemu.init("termwrap", 24, 80);
  setTitle("Not connected.");
  setmode("login");
  return;
}

function toggleshift() {
  termemu.toggleshift();
  keydiv = document.getElementById("shiftkey");
  if (termemu.shiftp())
    keydiv.className = "keysel";
  else
    keydiv.className = "key";
  return;
}

function togglectrl() {
  termemu.togglectrl();
  keydiv = document.getElementById("ctrlkey");
  if (termemu.ctrlp())
    keydiv.className = "keysel";
  else
    keydiv.className = "key";
  return;
}

function formlogin(ev) {
  ev.preventDefault();
  if (session.id != null)
    return;
  var loginmsg = {};
  loginmsg["name"] = document.getElementById("input_name").value;
  loginmsg["pw"] = document.getElementById("input_pw").value;
  var req = new XMLHttpRequest();
  req.onerror = errHandler;
  req.onreadystatechange = function () {
    if (req.readyState != 4 || req.status != 200) 
      return;
    var reply = JSON.parse(req.responseText);
    if (reply.t == 'l') {
      /* Success */
      session.lcred = reply.k;
      session.lname = reply.u;
      setTitle("Logged in as " + reply.u);
      debug(1, "Logged in as " + reply.u + " with id " + reply.k);
      setmode("choose");
    }
    else if (reply.t == 'E') {
      debug(1, "Could not log in: " + reply.s);
      document.getElementById("input_name").value = "";
      document.getElementById("input_pw").value = "";
    }
  };
  req.open('POST', '/login', true);
  req.send(JSON.stringify(loginmsg));
  return;
}

function getcurrent(clear) {
  if (session.id || clear) {
    if (statInterval) {
      window.clearInterval(statInterval);
      statInterval = null;
    }
    return;
  }
  if (!statInterval) {
    statInterval = window.setInterval(getcurrent, statDelta);
  }
  var req = new XMLHttpRequest();
  req.onerror = errHandler;
  req.onreadystatechange = function () {
    if (req.readyState != 4 || req.status != 200) 
      return;
    var reply;
    try {
      reply = JSON.parse(req.responseText);
    } catch (e) {
      if (e instanceof SyntaxError)
        return;
    }
    if (!reply.s) {
      return;
    }
    var gamediv = document.getElementById("gametable");
    while (gamediv.children.length > 2)
      gamediv.removeChild(gamediv.children[2]);
    if (reply.g.length === 0) {
      gamediv.style.display = "none";
      document.getElementById("nogames").style.display = "block";
    }
    else {
      gamediv.style.display = "table";
      document.getElementById("nogames").style.display = "none";
    }
    for (var i = 0; i < reply.g.length; i++) {
      var row = document.createElement("div");
      var cell1 = document.createElement("div");
      var cell2 = document.createElement("div");
      var cell3 = document.createElement("div");
      var cell4 = document.createElement("div");
      cell1.appendChild(document.createTextNode(reply.g[i].p));
      var uname = reply.g[i].g;
      if (uname in games)
        cell2.appendChild(document.createTextNode(games[uname].name));
      else {
        debug(1, "Unrecognized game: " + uname);
        continue;
      }
      cell3.appendChild(document.createTextNode(idlestr(reply.g[i].i)));
      var button = document.createElement("span");
      button.appendChild(document.createTextNode("Watch"));
      button.onclick = makeWatcher(reply.g[i].n);
      button.className = "ibutton";
      cell4.appendChild(button);
      row.appendChild(cell1);
      row.appendChild(cell2);
      row.appendChild(cell3);
      row.appendChild(cell4);
      gamediv.appendChild(row);
    }
  };
  req.open('GET', '/status', true);
  req.send();
  if (session.lcred)
    getchoices();
  return;
}

function getchoices() {
  if (session.id != null || !session.lcred)
    return;
  var req = new XMLHttpRequest();
  req.onerror = errHandler;
  req.onreadystatechange = function () {
    if (req.readyState != 4 || req.status != 200) 
      return;
    var reply;
    try {
      reply = JSON.parse(req.responseText);
    } catch (e) {
      if (e instanceof SyntaxError)
        return;
    }
    if (!("name" in reply) || reply["name"] != session.lname || 
        !("stat" in reply))
      return;
    var optdiv = document.getElementById("opttable");
    /* Don't remove the first child, it's the header. */
    while (optdiv.childNodes.length > 1)
      optdiv.removeChild(optdiv.childNodes[1]);
    for (var gname in games) {
      if (!(gname in reply.stat))
        continue;
      var acttext;
      if (reply.stat[gname] == "s")
        acttext = "Resume your game";
      else if (reply.stat[gname] == "0")
        acttext = "Start a game";
      else if (reply.stat[gname] == "p")
        acttext = "Game in progress";
      else
        continue;
      var button = document.createElement("span");
      button.appendChild(document.createTextNode(acttext));
      if ("s0".indexOf(reply.stat[gname]) >= 0) {
        button.onclick = makeStarter(gname);
        button.className = "ibutton";
      }
      var actdiv = document.createElement("div");
      actdiv.appendChild(button);
      var gamediv = document.createElement("div");
      gamediv.appendChild(document.createTextNode(games[gname].name));
      var rowdiv = document.createElement("div");
      rowdiv.appendChild(gamediv);
      rowdiv.appendChild(actdiv);
      optdiv.appendChild(rowdiv);
    }
  };
  req.open('GET', '/pstatus/' + session.lname, true);
  req.send();
  return;
}

/* This can't be in the loop in getchoices(), or the closure's scope will
 * get overwritten on the next iteration, and then all the games end up 
 * being Super-Rogue.
 */
function makeStarter(gname) {
  if (!(gname in games))
    return null;
  var game = games[gname];
  function starter(ev) {
    startgame(game);
  }
  return starter;
}

function startgame(game) {
  if (session.id != null || !session.lcred)
    return;
  var smsg = {};
  smsg["key"] = session.lcred;
  smsg["game"] = game.uname;
  smsg["h"] = 24;
  smsg["w"] = 80;
  var req = new XMLHttpRequest();
  req.onerror = errHandler;
  req.onreadystatechange = function () {
    if (req.readyState != 4 || req.status != 200) 
      return;
    var reply = JSON.parse(req.responseText);
    if (reply.t == 's') {
      /* Success */
      session.id = reply.id;
      session.playing = true;
      termemu.resize(reply.h, reply.w);
      setTitle("Playing as " + session.lname);
      debug(1, "Playing with id " + session.id);
      setmode("play");
      getData();
    }
    else if (reply.t == 'E') {
      debug(1, "Could not start game: " + reply.s);
      if (reply.c == 1) {
        logout();
      }
    }
  };
  req.open('POST', '/play', true);
  req.send(JSON.stringify(smsg));
  return;
}

function startwatching(gamenumber) {
  if (session.id != null)
    return;
  var wmsg = {"n": Number(gamenumber)};
  var req = new XMLHttpRequest();
  req.onerror = errHandler;
  req.onreadystatechange = function () {
    if (req.readyState != 4 || req.status != 200) 
      return;
    var reply = JSON.parse(req.responseText);
    if (reply.t == 'w') {
      /* Success */
      session.id = reply.id;
      session.playing = false;
      termemu.resize(reply.h, reply.w);
      termemu.reset();
      termemu.toAltBuf();
      setTitle("Watching");
      debug(1, "Watching with id " + session.id);
      setmode("play");
      getData();
    }
    else if (reply.t == 'E') {
      debug(1, "Could not watch game " + gamenumber + ": " + reply.s);
      getcurrent();
    }
  };
  req.open('POST', '/watch', true);
  req.send(JSON.stringify(wmsg));
  return;
}

function makeWatcher(n) {
  function watcher(ev) {
    startwatching(n);
  }
  return watcher;
}

function formreg(ev) {
  ev.preventDefault();
  if (session.id != null)
    return;
  var regmsg = {};
  regmsg["name"] = document.getElementById("regin_name").value;
  regmsg["pw"] = document.getElementById("regin_pw").value;
  regmsg["email"] = document.getElementById("regin_email").value;
  var req = new XMLHttpRequest();
  req.onerror = errHandler;
  req.onreadystatechange = function () {
    if (req.readyState != 4 || req.status != 200) 
      return;
    var reply = JSON.parse(req.responseText);
    if (reply.t == 'r') {
      /* Success */
      debug(1, "Registered account: " + reply.d);
      session.lcred = reply.k;
      session.lname = reply.u;
      setTitle("Logged in as " + session.lname);
      debug(1, "Logged in as " + session.lname + "with id " + session.lcred);
      setmode("choose");
    }
    else if (reply.t == 'E') {
      debug(1, "Could not register: " + reply.s);
      document.getElementById("regin_name").value = "";
      document.getElementById("regin_pw").value = "";
      document.getElementById("regin_email").value = "";
    }
  };
  req.open('POST', '/addacct', true);
  req.send(JSON.stringify(regmsg));
  return;
}

function gameover() {
  if (session.id == null)
    return;
  /* TODO IFACE2 If the end was unexpected, tell player the game was saved. */
  session.id = null;
  session.playing = false;
  ajaxstate.clear();
  setTitle("Game over.");
  termemu.toNormBuf();
  nsend = 0;
  nrecv = 0;
  msgQ = [];
  if (session.lcred != null)
    setmode("choose");
  else
    setmode("login");
  return;
}

function logout() {
  session.lcred = null;
  session.lname = null;
  setmode("login");
}

function stop() {
  if (!session.id)
    return;
  var req = new XMLHttpRequest();
  req.onerror = errHandler;
  req.onreadystatechange = function () {
    if (req.readyState == 4 && req.status == 200) {
      processMsg(req.responseText);
      return;
    }
  };
  req.open('POST', '/feed', true);
  req.send(JSON.stringify({"id": session.id, "t": "q"}));
  return;
}

function setmode(mode, ev) {
  if (ev)
    ev.preventDefault();
  if (mode == "play") {
    document.getElementById("keyboard").style.display = "block";
    document.getElementById("startgame").style.display = "none";
    document.getElementById("login").style.display = "none";
    document.getElementById("register").style.display = "none";
    document.getElementById("current").style.display = "none";
  }
  if (mode == "choose") {
    document.getElementById("keyboard").style.display = "none";
    document.getElementById("startgame").style.display = "block";
    document.getElementById("login").style.display = "none";
    document.getElementById("register").style.display = "none";
    document.getElementById("current").style.display = "block";
    getcurrent();
  }
  else if (mode == "login") {
    document.getElementById("keyboard").style.display = "none";
    document.getElementById("startgame").style.display = "none";
    document.getElementById("login").style.display = "block";
    document.getElementById("register").style.display = "none";
    document.getElementById("current").style.display = "block";
    getcurrent();
  }
  else if (mode == "register") {
    document.getElementById("keyboard").style.display = "none";
    document.getElementById("startgame").style.display = "none";
    document.getElementById("login").style.display = "none";
    document.getElementById("register").style.display = "block";
    document.getElementById("current").style.display = "none";
  }
}

function toggleBlock(id) {
  var element = document.getElementById(id);
  if (!element)
    return;
  if (element.style.display != "block")
    element.style.display = "block";
  else
    element.style.display = "none";
}

function debug(level, msg) {
  if (level < debugSuppress)
    return;
  var msgdiv = document.createElement("div");
  var msgtext = document.createTextNode(msg);
  msgdiv.appendChild(msgtext);
  document.getElementById("debug").appendChild(msgdiv);
  return;
}

function textsize(larger) {
  var cssSize = termemu.view.style.fontSize;
  if (!cssSize) {
    return;
  }
  var match = cssSize.match(/\d*/);
  if (!match) {
    return;
  }
  var csize = Number(match[0]);
  var nsize;
  if (larger) {
    if (csize >= 48)
      nsize = 48;
    else if (csize >= 20)
      nsize = csize + 4;
    else if (csize >= 12)
      nsize = csize + 2;
    else if (csize >= 8)
      nsize = csize + 1;
    else
      nsize = 8;
  }
  else {
    if (csize <= 8)
      nsize = 8;
    else if (csize <= 12)
      nsize = csize - 1;
    else if (csize <= 20)
      nsize = csize - 2;
    else if (csize <= 48)
      nsize = csize - 4;
    else
      nsize = 48;
  }
  document.getElementById("term").style.fontSize = nsize.toString() + "px";
  termemu.fixsize();
  debug(1, "Changing font size to " + nsize.toString());
  return;
}

function idlestr(ms) {
  if (typeof(ms) != "number")
    return "?";
  var seconds = Math.round(ms / 1000);
  var ss = String(seconds % 60);
  if (ss.length < 2)
    ss = "0" + ss;
  var mm = String(Math.floor((seconds % 3600) / 60));
  if (mm.length < 2)
    mm = "0" + mm;
  var hh = String(Math.floor(seconds / 3600));
  if (hh.length < 2)
    hh = "0" + hh;
  return hh + ":" + mm + ":" + ss;
}

function bell(on) {
  var imgnode = document.getElementById("bell");
  if (on) {
    imgnode.style.visibility = "visible";
    window.setTimeout(bell, 1500, false);
  }
  else
    imgnode.style.visibility = "hidden";
  return;
}