2016年4月26日火曜日

御城プロジェクト:REをほんのり快適にするスクリプト 城福.js

GreaseMonkeyを使った御城プロジェクト:RE拡張スクリプトを書きました.
導入すると以下の機能が追加されます.
  • BGMとボイス・SEのボリューム調整機能
    ボイスとSEは共通です.
  • ゲーム画面のスクリーンショット取得機能
    自動的にIDをマスクすることもできます.
  • ゲームの描画処理を若干軽量化
    環境にも依りますが,描画処理が滑らかになります.スクロール処理などで顕著.
  • ゲームの描画品質の変更機能
    アンチエイリアスを切ることで描画速度が改善する可能性があります.
  • スクリーンサイズの変更が出来ます
    50%から10%刻みの6段階の変更が可能です.
    (タッチ操作は一切考慮に入れてないから正しく動作しないかも…)
  • フルスクリーンへの切り替えを追加してみました
    esc/f11キーの他, 画面右上をクリックすることで復帰可能にしています.
  • フレームスキップ機構を追加してみました
    処理が重い環境で試してみると良いかもしれません.

※いずれもサーバー側には一切影響を与えないようにしています.
※チートツールではありません.あくまで,インターフェースの機能改善ツールであって,ゲームの中身には一切触れていません.また,筆者は自動化とかには興味ありません.
※今後パフォーマンス改善やボリューム設定機構がゲーム側で実装された場合,本スクリプトが競合を引き起こすかもしれません.(ゲームのリサイズ機能が実装されたら多分死ぬ)
※現状に不満がなければ無理に導入するものでもありません.どうしてもこれが必要な場合に限って渋々入れてください.

(zoom機能は無理っぽいな…イベントがゲームのフレーム外で管理されている気がする.)



動作環境:
  • firefox + GreaseMonkey
    動作チェックはubuntu 14.04 64bit + firefox 47 64bitで行っています.
    変なことはしていないからwindowsでも動くんじゃね?
  • HTML5の仕組みしか使っていないので,choromeでも動くかもしれない
    chrome + Tampermonkey環境でも動くっぽ
  • ie?edge?safari?知らね

ライセンス
  • MIT
    ご自由に.その代わり作者は一切の責任を負いません.コードを読んで変なことをしていないことを確認し,その危険性についてよく理解した上でご利用ください.
既知の問題
  • 右クリックメニューが有効となってしまう.
    ゲーム上差し支えないものの,挙動が変化してしまうのはあまり嬉しくない.

ファイル
http://www.h2.dion.ne.jp/~defghi/shiro/shiro_fuku.user.js
↓新URL
http://defghi1977.html.xdomain.jp/tech/shiro/shiro_fuku.user.js
※GreaseMonkeyが有効だと勝手にインストール機構が動作する模様.

以下コード

// ==UserScript==
// @name        shiro_fuku
// @namespace   defghi1977
// @description 御城プロジェクト:REライフをほんのり快適にします
// @include     http://assets.shiropro-re.net/html/Oshiro.html
// @include     http://osapi.dmm.com/gadgets/ifr*
// @include     http://www.dmm.com/netgame/social/-/gadgets/=/app_id=777106/
// @version     4.0
// @grant       none
// ==/UserScript==
/*
written by DEFGHI1977
http://defghi1977-onblog.blogspot.jp/2016/04/re-js.html
追加される機能
・グラフィック描画タイミングの調整
 ゲーム画面の描画が若干滑らかになります.
 ※トレードオフとして消費メモリが若干増加しています.
・BGM/効果音/音声ボリューム調整機能
・グラフィック描画品質変更機能
 アンチエイリアス処理を切ることでゲームの処理負荷が軽減されるかもしれません.
 ※環境によっては却って処理速度が犠牲になる可能性もあります.
・ゲーム画面のサイズ変更機能
 50%から10%刻みでの6段階で設定できます.ブラウザ側でのズーム指定をする必要がなくなりました.
・ゲームのフルスクリーン化
 esc/F11キー等で元に戻せます.
・ゲームのスクリーンショットを撮ることが出来ます.
 自動的にIDにマスクをかけることも出来ます.
*/
'use strict';
//ready to fullscreen
if (/http:\/\/www\.dmm\.com\/netgame\/social\/-\/gadgets\/=\/app_id=777106\//.test(location.href)) {
  (function () {
    var iframe = document.querySelector('#game_frame');
    iframe.setAttribute('allowfullscreen', 'true');
    iframe.allowfullscreen = true;
  }) ();
  return;
}
if (/http:\/\/osapi\.dmm\.com\/gadgets\/ifr/.test(location.href)) {
  (function () {
    var mo = new MutationObserver(function (mutations) {
      mutations.every(function (mu) {
        for (var i = 0, len = mu.addedNodes.length; i < len; i++) {
          var node = mu.addedNodes[i];
          if (node.id == 'oshiro') {
            node.setAttribute('allowfullscreen', 'true');
            node.allowfullscreen = true;
            mo.disconnect();
            return false;
          }
        }
        return true;
      });
    });
    mo.observe(document, {
      childList: true,
      subtree: true
    });
  }) ();
  return;
} //main panel setting
(function () {
  var version = '4.0';
  var appName = '🏯城福';
  var panel = document.createElement('div');
  panel.className = 'shiro_fuku';
  var s = panel.style;
  s.fontSize = '10px';
  s.fontWeight = 'bold';
  s.textAlign = 'left';
  s.color = 'white';
  s.textShadow = '1px 1px 0 navy';
  s.lineHeight = '30px';
  document.body.insertBefore(panel, document.querySelector('.emscripten_border').nextElementSibling);
  var ui = document.createElement('div');
  ui.className = 'shiro_ui';
  ui.textContent = appName + 'v' + version + ' ';
  var su = ui.style;
  su.position = 'relative';
  su.zIndex = '1';
  su.paddingLeft = '3px';
  panel.appendChild(ui);
}) ();
//shiro_volume
(function (proto) {
  var keyBGM = 'defghi1977.shiro_vol.bgm';
  var keyEFC = 'defghi1977.shiro_vol.effect';
  var audios = [
  ];
  //ui settings
  var ui = document.querySelector('.shiro_ui');
  var rangeBGM = document.createElement('input');
  rangeBGM.type = 'range';
  rangeBGM.min = 0;
  rangeBGM.max = 1;
  rangeBGM.step = 0.1;
  rangeBGM.title = 'BGMのボリュームを調整します';
  rangeBGM.style.width = '75px';
  rangeBGM.style.height = '1em';
  rangeBGM.value = localStorage.getItem(keyBGM) || 1;
  var rangeEFC = rangeBGM.cloneNode(false);
  rangeEFC.value = localStorage.getItem(keyEFC) || 1;
  rangeEFC.title = '効果音ボリュームを調整します';
  //add ui
  ui.appendChild(document.createTextNode('BG:'));
  ui.appendChild(rangeBGM);
  ui.appendChild(document.createTextNode('EF:'));
  ui.appendChild(rangeEFC);
  //events
  function getEventListener(key) {
    return function (e) {
      localStorage.setItem(key, this.value);
      syncAll();
    }
  }
  rangeBGM.addEventListener('input', getEventListener(keyBGM));
  rangeEFC.addEventListener('input', getEventListener(keyEFC));
  //overrides to change volume when call play method
  var op = proto.play;
  proto.play = function () {
    if (!this._captured) {
      audios.push(this);
      this._captured = true;
    }
    sync(this);
    return op.apply(this, arguments);
  }
  function syncAll() {
    audios.forEach(sync);
  } //sound effects

  function sync(audio) {
    audio.volume = audio.loop ? rangeBGM.value : rangeEFC.value;
  }
}) (HTMLAudioElement.prototype);
//shiro_skip & shiro_qa
(function () {
  var width = 1280;
  var height = 720;
  //append interface
  var ui = document.querySelector('.shiro_ui');
  ui.appendChild(document.createTextNode('QA:'));
  var qa = document.createElement('input');
  qa.type = 'checkbox';
  qa.checked = true;
  qa.disabled = true;
  qa.title = '描画品質を変更します';
  ui.appendChild(qa);
  ui.appendChild(document.createTextNode('FS:'));
  var selSkip = document.createElement('select');
  selSkip.innerHTML = '<option value="1">Full</option>'
  + '<option value="2">1/2</option>'
  + '<option value="3">1/3</option>';
  selSkip.title = '描画頻度を変更します';
  selSkip.disabled = true;
  ui.appendChild(selSkip);
  //customized screen
  var panel = document.querySelector('.shiro_fuku');
  var screen = document.createElement('canvas');
  screen.id = 'shiro_screen';
  screen.width = width;
  screen.height = height;
  screen.addEventListener('contextmenu', function (e) {
    e.preventDefault();
  });
  var s = screen.style;
  s.transform = 'translateZ(0)'; //force gpu
  s.display = 'block';
  s.boxShadow = '0 30px 0 0 indigo';
  s.position = 'relative';
  s.zIndex = 0;
  panel.insertBefore(screen, panel.firstChild);
  //hide original screen
  document.querySelector('.emscripten_border').style.display = 'none';
  //find inmemory screen
  var proto = HTMLCanvasElement.prototype;
  var ogc = proto.getContext;
  var buffer;
  var bctx;
  proto.getContext = function () {
    var ctx;
    if (!buffer && !this.parentNode && this.width == width && this.height == height) {
      //kill opacity of buffer(to lighten rendering)
      ctx = ogc.call(this, '2d', {
        alpha: false
      });
      buffer = this;
      bctx = ctx;
      init();
    } else {
      ctx = ogc.apply(this, arguments);
    }
    return ctx;
  };
  function init() {
    //add quality changer
    (function () {
      qa.addEventListener('change', function () {
        [
          'imageSmoothingEnabled',
          'mozImageSmoothingEnabled',
          'webkitImageSmoothingEnabled',
          'msImageSmoothingEnabled'
        ].forEach(function (prop) {
          bctx[prop] = qa.checked;
        });
      });
      qa.disabled = false;
    }) ();
    //render width frame skip
    (function () {
      var ra = requestAnimationFrame || mozRequestAnimationFrame || webkitRequestAnimationFrame || msRequestAnimationFrame;
      var sctx = screen.getContext('2d', {
        alpha: false
      });
      var fr = 0;
      var skip = 1;
      var draw = drawF;
      selSkip.addEventListener('change', setSkip);
      selSkip.addEventListener('keyup', setSkip);
      function setSkip() {
        skip = selSkip.value * 1;
        draw = skip == 1 ? drawF : drawS;
      }
      setSkip();
      selSkip.disabled = false;
      function transfer() {
        sctx.drawImage(buffer, 0, 0, screen.width, screen.height);
      }
      function drawF() {
        transfer();
        ra(draw);
      }
      function drawS() {
        if (fr++ % skip == 0) {
          transfer();
        }
        ra(draw);
      } //start rendering

      draw();
    }) ();
    //ignore rendering to original screen
    var canvas = document.querySelector('#canvas');
    var ctx = canvas.getContext('2d');
    ctx.drawImage = ctx.clearRect = ctx.putImageData = function () {
    };
  }
}) ();
//shiro_zoom
(function () {
  var key = 'defghi1977.shiro_zoom.value';
  var panel = document.querySelector('.shiro_fuku');
  var eb = document.querySelector('.emscripten_border');
  var screen = document.querySelector('#shiro_screen');
  var sstyle = screen.style;
  var ui = document.querySelector('.shiro_ui');
  ui.appendChild(document.createTextNode('ZM:'));
  var selZoom = document.createElement('select');
  selZoom.title = 'スクリーンの表示倍率を指定します';
  selZoom.innerHTML = '<option value="1">100%</option>'
  + '<option value="0.9">90%</option>'
  + '<option value="0.8">80%</option>'
  + '<option value="0.7">70%</option>'
  + '<option value="0.6">60%</option>'
  + '<option value="0.5">50%</option>';
  //screen zoom
  var zoom = localStorage.getItem(key) * 1 || 1;
  var xzoom;
  var yzoom;
  selZoom.value = zoom;
  apply();
  selZoom.addEventListener('change', apply);
  selZoom.addEventListener('keyup', apply);
  function apply() {
    xzoom = yzoom = zoom = selZoom.value * 1;
    localStorage.setItem(key, zoom);
    sstyle.width = screen.width * xzoom + 'px';
    sstyle.height = screen.height * yzoom + 'px';
  }
  ui.appendChild(selZoom);
  //emulate mouse events
  [
    'mousedown',
    'mouseup',
    'mouseover',
    'mouseout',
    'mousemove',
    'click',
    'dblclick',
    'wheel'
  ].forEach(function (type) {
    panel.addEventListener(type, cancelEvents);
    screen.addEventListener(type, createEmulator(type));
  });
  function cancelEvents(e) {
    e.stopPropagation();
  }
  function createEmulator(type) {
    var translate = getTranslator(type);
    return function (e) {
      e.stopPropagation();
      e.preventDefault();
      eb.dispatchEvent(translate(e));
    };
  }
  function getTranslator(type) {
    var eConstructor = type == 'wheel' ? WheelEvent : MouseEvent;
    var param = {
      //by Event
      bubbles: null,
      cancelable: null,
      //by UIEvent
      detail: null,
      view: null,
      //by MouseEvent
      screenX: null,
      screenY: null,
      clientX: null,
      clientY: null,
      ctrlKey: null,
      shiftKey: null,
      altKey: null,
      metaKey: null,
      button: null,
      buttons: null,
      relatedTarget: null,
      region: null,
      //by WheelEvent
      deltaX: null,
      deltaY: null,
      deltaZ: null,
      deltaMode: null
    };
    return function (e) {
      for (var i in param) {
        param[i] = e[i];
      }
      param.clientX = (param.clientX - 5) / xzoom + 5;
      param.clientY /= yzoom;
      param.relatedTarget = document.body;
      return new eConstructor(type, param);
    }
  } //fullscreen

  document.addEventListener('fullscreenchange', onfullscreenchange);
  document.addEventListener('webkitfullscreenchange', onfullscreenchange);
  document.addEventListener('msfullscreenchange', onfullscreenchange);
  function onfullscreenchange() {
    if (document.fullscreenElement || document.webkitFullscreenElement || document.msFullscreenElement) {
      //spread screen
      sstyle.width = '100%';
      sstyle.height = '100%';
      var cs = getComputedStyle(screen);
      var w = cs.width.replace(/px/, '') * 1;
      var h = cs.height.replace(/px/, '') * 1;
      //culcurate zoom
      xzoom = w / screen.width;
      yzoom = h / screen.height;
    } else {
      //reset zoom
      apply();
    }
  }
}) ();
//shiro_fullscreen
(function () {
  var panel = document.querySelector('.shiro_fuku');
  var screen = document.querySelector('#shiro_screen');
  var ui = document.querySelector('.shiro_ui');
  var fs = document.createElement('input');
  fs.type = 'button';
  fs.title = '画面をフルスクリーンに切り替えます';
  fs.value = 'FS';
  ui.appendChild(fs);
  fs.addEventListener('click', function () {
    try {
      if (screen.requestFullscreen) {
        screen.requestFullscreen();
      } else if (screen.webkitRequestFullScreen) {
        screen.webkitRequestFullScreen();
      } else {
        return;
      }
      exit.style.display = 'block';
    } catch (e) {
    }
  });
  screen.addEventListener('click', function(e){
    console.log([window.screen.width - e.clientX,window.screen.height - e.clientY]);
    var isFullscreen = document.fullscreenElement || document.webkitFullscreenElement || document.msFullscreenElement;
    if(isFullscreen && window.screen.width - e.screenX < 25 && e.screenY < 25){
      if (document.exitFullscreen) {
        document.exitFullscreen()
      } else if (document.webkitExitFullscreen) {
        document.webkitExitFullscreen();
      }
    }
  });
}) ();
//shiro_shot
(function () {
  var ui = document.querySelector('.shiro_ui');
  var button = document.createElement('input');
  button.type = 'button';
  button.value = 'as is';
  button.title = '画面をそのまま出力します';
  button.addEventListener('click', shot);
  var buttonM = document.createElement('input');
  buttonM.type = 'button';
  buttonM.value = 'mask';
  buttonM.title = 'IDを隠します';
  buttonM.addEventListener('click', shot);
  var link = document.createElement('a');
  link.download = 'shiro_shot.png';
  var canvas = document.createElement('canvas');
  ui.appendChild(document.createTextNode('SS:'));
  ui.appendChild(button);
  ui.appendChild(buttonM);
  ui.appendChild(link);
  function shot(e) {
    var game = document.querySelector('#shiro_screen') || document.querySelector('#canvas');
    canvas.width = game.width;
    canvas.height = game.height;
    var ctx = canvas.getContext('2d');
    ctx.drawImage(game, 0, 0);
    if (e.target === buttonM) {
      ctx.fillStyle = '#2e0f06';
      ctx.fillRect(210, 5, 210, 24);
    }
    if (link.href) {
      URL.revokeObjectURL(link.href);
    }
    canvas.toBlob(function (blob) {
      link.href = URL.createObjectURL(blob);
      link.click();
    }, 'image/png');
  }
}) ();

0 件のコメント:

コメントを投稿