├── .babelrc ├── .gitignore ├── README.md ├── dist ├── script.js └── style.css ├── font └── ikamodoki1_0.ttf ├── img ├── bg.png └── icon.png ├── index.html ├── package.json ├── sh ├── bump.sh └── deploy.sh └── src ├── script ├── component │ ├── input │ │ ├── kd-input.jsx │ │ ├── rate-input.jsx │ │ ├── result-input.jsx │ │ ├── result-option-input.jsx │ │ ├── rule-input.jsx │ │ ├── save-btn.jsx │ │ ├── single-stage-input.jsx │ │ └── stage-input.jsx │ ├── record │ │ ├── kd-graph.jsx │ │ ├── list.jsx │ │ ├── mod-popup.jsx │ │ ├── switcher.jsx │ │ └── udemae-graph.jsx │ └── stat │ │ ├── recent-stat.jsx │ │ ├── total-stat.jsx │ │ └── win-rate-stat.jsx ├── const.js ├── main.js ├── model │ ├── _base.js │ ├── record.js │ └── user.js ├── page │ ├── _master.jsx │ ├── input.jsx │ ├── others.jsx │ ├── record.jsx │ ├── record │ │ ├── graph.jsx │ │ └── list.jsx │ └── stat.jsx ├── reducer │ ├── record │ │ ├── graph.js │ │ └── list.js │ └── stat.js ├── router.jsx └── util.js └── style ├── _app.scss ├── _common.scss ├── _input.scss ├── _record.scss ├── _reset.scss ├── _stat.scss └── main.scss /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015", 4 | "react" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ウデマエアーカイブ 2 | ウデマエの上がり下がりを可視化する取り組みです。 3 | 4 | 個人用の記録アプリとしてスタートしましたが、意外に使ってくれてるイカが多くて驚いている今日このごろ。 5 | 引続きイカよろしく! 6 | 7 | いろんな声をもらったりもしますが、今のところのやらないコトは[こちら](https://github.com/leader22/ika-rchive/issues/17)。 8 | 9 | ## How to install 10 | ```sh 11 | # git cloneしてcdして 12 | npm i 13 | npm run build 14 | ``` 15 | 16 | ここまですればビルド完了です。 17 | 18 | 適当にサーバー立てて`index.html`へアクセスすれば見れます。 19 | 20 | ```sh 21 | # たとえば 22 | python -m SimpleHTTPServer 8080 23 | ``` 24 | 25 | ## ライセンス的な 26 | MITです。 27 | 勝手にForkして勝手にホストしてもらっても別に良いです。 28 | -------------------------------------------------------------------------------- /dist/style.css: -------------------------------------------------------------------------------- 1 | *{margin:0;padding:0;box-sizing:border-box}html,body{height:100%;background-color:#000;background-image:url(../img/bg.png);background-repeat:no-repeat;background-size:cover;color:#fff}html{line-height:1.2}body{font-family:"Helvetica Neue", Helvetica, Arial, Roboto, "Droid Sans", "メイリオ", Meiryo, "MS Pゴシック", "ヒラギノ角ゴ Pro W3", "Hiragino Kaku Gothic ProN", sans-serif}h1,h2{text-align:center}h1{padding-top:0.5em !important;font-size:1.8em}h2{margin:.25em auto}p{margin:.25em 1em}a{color:#fff}ul{list-style:none}.view-stat .tweet-button,.view-input .set-button,.view-input .wait-button,.view-input .reaction-button,.view-app .intro-button{display:block;width:300px;max-width:300px;line-height:40px;height:40px;margin:0 auto;border:none;background-color:#FF6E00;color:#fff;font-size:1.3em;font-weight:bold;text-decoration:none;text-align:center;cursor:pointer}.view-stat .tweet-button:hover,.view-input .set-button:hover,.view-input .wait-button:hover,.view-input .reaction-button:hover,.view-app .intro-button:hover{opacity:.8}.view-stat .tweet-button:active,.view-input .set-button:active,.view-input .wait-button:active,.view-input .reaction-button:active,.view-app .intro-button:active{transform:translateY(1px)}.view-stat [disabled].tweet-button,.view-input [disabled].set-button,.view-input [disabled].wait-button,.view-input [disabled].reaction-button,.view-app [disabled].intro-button{cursor:default}.h3,.view-record .record-list,.view-stat .user-stat,.view-input .input-wrap{margin:1em auto;width:90%}@font-face{font-family:'ikamodoki';src:url("../font/ikamodoki1_0.ttf") format("truetype")}.ft-ika{font-family:'ikamodoki';vertical-align:text-bottom;padding:0 2px}.ft-emp{font-weight:900}.fs-s{font-size:.85em}.fc-rule-1{color:#FF6E00}.fc-rule-2{color:#00B7A4}.fc-rule-3{color:#CA00DF}.wrap{padding:.5em;margin:1em;background-color:rgba(0,0,0,0.5);border-radius:.5em}.h3{margin-bottom:0}.note{margin:.25em 1em;font-size:.5em}.list{list-style:square inside;padding:.25em 1em}.list li+li{margin-top:.5em}.view-record .switcher{text-align:center;padding:.25em 0}.view-record .graph-wrap{padding-top:.5em;margin:1em auto;text-align:center;overflow-y:auto;-webkit-overflow-scrolling:touch;background-color:rgba(0,0,0,0.5)}.view-record .graph-cover{text-align:center;padding-top:60px}.view-record .graph{position:relative}.view-record .graph-legend{position:absolute;right:.75em;top:.5em;font-size:.75em;text-align:center}.view-record .graph-legend>span+span{margin-left:.5em}.view-record .record-list-item+.record-list-item{margin-top:1em;padding-top:1em;border-top:1px solid rgba(255,255,255,0.8)}.view-record .record-list-item-spacer{height:.5em;border:none}.view-record .ctrl-wrap{font-size:.5em;text-align:right}.view-record .mod-mark,.view-record .del-mark{cursor:pointer;text-decoration:underline}.view-record .mod-layer{position:fixed;top:0;left:0;right:0;bottom:0;z-index:1;background-color:rgba(0,0,0,0.8)}.view-record .mod-wrap{position:absolute;top:10%;left:0;right:0;text-align:center;background-color:rgba(0,0,0,0.8)}.view-record .mod-wrap .set-button,.view-record .mod-wrap .reaction-button{width:100%}.view-stat .user-stat{line-height:1.5em}.view-stat .user-stat td{width:50%}.view-stat .user-stat td.slim{width:25%;text-align:right}.view-stat .user-stat td+td{text-indent:.5em}.view-stat .tweet-button{margin:1em auto;background-color:#50ABF1}.view-input .input-item{text-align:center}.view-input .input-item+.input-item{margin-top:.75em}.view-input .input-item.m-title{font-size:.75em;text-align:left;cursor:pointer}.view-input input[type=radio],.view-input input[type=checkbox]{margin-right:.2em}.view-input select{font-size:1em;border-radius:1px;border:none}.view-input input[type=number]{width:60px;font-size:1em;text-align:center;padding:.25em .5em;margin-left:.5em;border:none}.view-input label{margin:0 .2em .5em;display:inline-block}.view-input .stage-select+.stage-select{margin-top:.5em}.view-input .stage-select select{margin-left:1em}.view-input .set-button,.view-input .wait-button,.view-input .reaction-button{margin:1em auto}.view-input .wait-button{opacity:.8}.view-input .reaction-button{background-color:#77EF00}.view-input .report{text-align:center;font-size:.8em;margin-bottom:-1em}.view-app{position:relative;height:auto;min-height:100%;padding-bottom:40px}.view-app .footer{position:absolute;bottom:0;left:0;right:0;padding:.5em 0;text-align:center;font-size:.8em}.view-app .intro-button{margin:1em auto}.view-app .title{font-size:1.2em}.view-app .tab{text-align:center;margin:.75em auto;font-size:1.3em}.view-app .tab-item{display:inline-block;cursor:pointer;width:25%}.view-app .tab-item>a{display:block;text-decoration:none}.view-app .tab-item>a:active{transform:scale(1.1)}.view-app .tab-item>a.is-active{color:#77EF00}.view-body{opacity:0;transition:opacity .5s ease;will-change:opacity}.view-body.is-booted{opacity:1} 2 | -------------------------------------------------------------------------------- /font/ikamodoki1_0.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leaysgur/ika-rchive/25fee0eb2eb0117fc37e442c6a1e1a0c2d56378b/font/ikamodoki1_0.ttf -------------------------------------------------------------------------------- /img/bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leaysgur/ika-rchive/25fee0eb2eb0117fc37e442c6a1e1a0c2d56378b/img/bg.png -------------------------------------------------------------------------------- /img/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leaysgur/ika-rchive/25fee0eb2eb0117fc37e442c6a1e1a0c2d56378b/img/icon.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | ウデマエアーカイブ 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 |
26 |

ウデマエア-カイブ

27 |
28 |

ガチマッチウデマエを記録できるサービスだぞ!

29 |

1戦ごとに勝敗を記録して、グラフを眺めてニヤニヤしたり、苦手なステージを把握したり、連敗の兆候を感じ取ったりしよう!

30 |
31 | 32 |

チュウイ

33 |
34 |

登録不要・ログインいらずで使えます。

35 |

ただデータは直近の200戦分のみを、この端末の中に保存します。(localStorage)

36 |

なので、他の端末にデータは引き継ぎ・引っ越しできません。

37 |

予告なくサービスの内容を変更したり、終了したりするかもしれません。

38 |

もしかすると動作しない環境もあるかもしれません・・。

39 |

まぁそんな適当な感じで良ければ使ってください。

40 |

イカ、よろしく〜〜〜

41 |
42 | 43 |

サクシャ

44 |
45 |

なにかあれば@leader22まで!

46 |
47 | 48 | 49 |
50 | 51 | 52 |
53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ika-rchive", 3 | "version": "2.0.3", 4 | "description": "Archive your UDEMAE!", 5 | "private": "true", 6 | "main": "index.js", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1", 9 | "deploy": "./sh/deploy.sh", 10 | "dev-js": "mkdir -p ./dist && watchify src/script/main.js -t babelify -o dist/script.js -v", 11 | "dev-css": "mkdir -p ./dist && node-sass -w src/style/main.scss dist/style.css", 12 | "build": "npm run build-js && npm run build-css", 13 | "build-js": "mkdir -p ./dist && browserify src/script/main.js -t babelify -t uglifyify -o dist/script.js", 14 | "build-css": "mkdir -p ./dist && node-sass --output-style compressed src/style/main.scss dist/style.css" 15 | }, 16 | "author": "leader22", 17 | "license": "MIT", 18 | "dependencies": { 19 | "chart.js": "^2.1.2", 20 | "object-assign": "^4.1.0", 21 | "react": "^15.0.2", 22 | "react-dom": "^15.0.2", 23 | "react-router": "^2.4.0", 24 | "react-tap-event-plugin": "^1.0.0" 25 | }, 26 | "devDependencies": { 27 | "babel-preset-es2015": "^6.6.0", 28 | "babel-preset-react": "^6.5.0", 29 | "babelify": "^7.3.0", 30 | "browserify": "^13.0.1", 31 | "node-sass": "^4.14.1", 32 | "uglifyify": "^3.0.1", 33 | "watchify": "^3.7.0" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /sh/bump.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | old=$1; 3 | new=$2; 4 | 5 | if [ $# -ne 2 ]; then 6 | echo "Usage:" 7 | echo " ./sh/bump.sh " 8 | exit 1 9 | fi 10 | 11 | echo "$old -> $new" 12 | 13 | re="s/$old/$new/g" 14 | 15 | perl -pi -e ${re} package.json 16 | perl -pi -e ${re} index.html 17 | -------------------------------------------------------------------------------- /sh/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | git fetch -p 3 | git merge origin/master 4 | npm run build 5 | -------------------------------------------------------------------------------- /src/script/component/input/kd-input.jsx: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | 3 | const KDInput = ({ 4 | kill, death, 5 | onChange, 6 | }) => { 7 | return ( 8 |
9 | { onChange('kill', ev.target.value); }} 14 | /> k 15 | / 16 | { onChange('death', ev.target.value); }} 21 | /> d 22 |
23 | ); 24 | }; 25 | 26 | KDInput.propTypes = { 27 | kill: React.PropTypes.string.isRequired, 28 | death: React.PropTypes.string.isRequired, 29 | onChange: React.PropTypes.func.isRequired, 30 | }; 31 | 32 | module.exports = KDInput; 33 | -------------------------------------------------------------------------------- /src/script/component/input/rate-input.jsx: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | 3 | const { 4 | RATE_TABLE, 5 | MIN_RATE_INPUT, MAX_RATE_INPUT, 6 | } = require('../../const'); 7 | 8 | const RateInput = ({ 9 | rateRank, 10 | rateScore, _rateScore, 11 | onChange, 12 | }) => { 13 | return ( 14 |
15 | 28 | 29 | { onChange('rateScore', ev.target.value); }} 35 | /> 36 |
37 | ); 38 | }; 39 | 40 | RateInput.propTypes = { 41 | rateRank: React.PropTypes.string.isRequired, 42 | rateScore: React.PropTypes.string.isRequired, 43 | _rateScore: React.PropTypes.string.isRequired, 44 | onChange: React.PropTypes.func.isRequired, 45 | }; 46 | 47 | module.exports = RateInput; 48 | -------------------------------------------------------------------------------- /src/script/component/input/result-input.jsx: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | 3 | const { RESULT, } = require('../../const'); 4 | 5 | const ResultInput = ({ 6 | result, 7 | onChange, 8 | }) => { 9 | return ( 10 |
11 | {Object.keys(RESULT).map((key, idx) => { 12 | return ( 13 | 20 | ); 21 | })} 22 |
23 | ); 24 | }; 25 | 26 | ResultInput.propTypes = { 27 | result: React.PropTypes.string.isRequired, 28 | onChange: React.PropTypes.func.isRequired, 29 | }; 30 | 31 | module.exports = ResultInput; 32 | -------------------------------------------------------------------------------- /src/script/component/input/result-option-input.jsx: -------------------------------------------------------------------------------- 1 | const React = require('react'); // eslint-disable-line no-unused-vars 2 | 3 | const ResultOptionInput = ({ 4 | tagmatch, 5 | missmatch, 6 | isDisconnected, 7 | onChange, 8 | }) => { 9 | return ( 10 |
11 | 19 | 20 | 27 |
28 | ); 29 | }; 30 | 31 | ResultOptionInput.propTypes = { 32 | tagmatch: React.PropTypes.bool.isRequired, 33 | missmatch: React.PropTypes.bool.isRequired, 34 | isDisconnected: React.PropTypes.bool.isRequired, 35 | onChange: React.PropTypes.func.isRequired, 36 | }; 37 | 38 | module.exports = ResultOptionInput; 39 | -------------------------------------------------------------------------------- /src/script/component/input/rule-input.jsx: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | 3 | const { RULE, } = require('../../const'); 4 | 5 | const RuleInput = ({ 6 | rule, 7 | onChange, 8 | }) => { 9 | return ( 10 |
11 | {Object.keys(RULE).map((key, idx) => { 12 | return ( 13 | 20 | ); 21 | })} 22 |
23 | ); 24 | }; 25 | 26 | RuleInput.propTypes = { 27 | rule: React.PropTypes.string.isRequired, 28 | onChange: React.PropTypes.func.isRequired, 29 | }; 30 | 31 | module.exports = RuleInput; 32 | -------------------------------------------------------------------------------- /src/script/component/input/save-btn.jsx: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | 3 | let _timer = null; 4 | 5 | class SaveBtn extends React.Component { 6 | constructor() { 7 | super(); 8 | 9 | this.state = { 10 | showReaction: false 11 | }; 12 | 13 | this.onSave = this.onSave.bind(this); 14 | } 15 | 16 | onSave() { 17 | const { 18 | canInput, 19 | onSave, onAfterSave, 20 | } = this.props; 21 | const setState = this.setState.bind(this); 22 | 23 | if (!canInput) { return; } 24 | 25 | onSave(); 26 | setState({ showReaction: true }); 27 | _timer = setTimeout(function() { 28 | setState({ showReaction: false }); 29 | onAfterSave(); 30 | }, 1000); 31 | } 32 | 33 | componentWillUnmount() { 34 | clearTimeout(_timer); 35 | _timer = null; 36 | } 37 | 38 | render() { 39 | const { canInput, } = this.props; 40 | const { showReaction, } = this.state; 41 | 42 | let state = 'wait', 43 | label = 'ニュウリョクチュウ...'; 44 | 45 | if (canInput) { 46 | state = 'set', 47 | label = 'トウロク!'; 48 | } 49 | 50 | if (showReaction) { 51 | state = 'reaction', 52 | label = 'カンリョウ!'; 53 | } 54 | 55 | return ( 56 | 61 | ); 62 | } 63 | } 64 | 65 | SaveBtn.propTypes = { 66 | canInput: React.PropTypes.bool.isRequired, 67 | onSave: React.PropTypes.func.isRequired, 68 | onAfterSave: React.PropTypes.func, 69 | }; 70 | 71 | SaveBtn.defaultProps = { 72 | onAfterSave: () => {}, 73 | }; 74 | 75 | module.exports = SaveBtn; 76 | -------------------------------------------------------------------------------- /src/script/component/input/single-stage-input.jsx: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | 3 | const { STAGE, } = require('../../const'); 4 | 5 | const SingleStageInput = ({ 6 | stage, 7 | onChange, 8 | }) => { 9 | return ( 10 |
11 | 24 |
25 | ); 26 | }; 27 | 28 | SingleStageInput.propTypes = { 29 | stage: React.PropTypes.string.isRequired, 30 | onChange: React.PropTypes.func.isRequired, 31 | }; 32 | 33 | module.exports = SingleStageInput; 34 | -------------------------------------------------------------------------------- /src/script/component/input/stage-input.jsx: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | 3 | const { STAGE, } = require('../../const'); 4 | const STAGE_AB = ['stageA', 'stageB']; 5 | 6 | const StageInput = ({ 7 | stage, stageAandB, 8 | onChange, 9 | }) => { 10 | return ( 11 |
12 | {STAGE_AB.map((stageAorB, idx) => { 13 | return ( 14 |
15 | { onChange('stage', ev.target.value); }} 19 | /> 20 | 33 |
34 | ); 35 | })} 36 |
37 | ); 38 | }; 39 | 40 | StageInput.propTypes = { 41 | stage: React.PropTypes.oneOf(STAGE_AB).isRequired, 42 | stageAandB: React.PropTypes.shape({ 43 | [STAGE_AB[0]]: React.PropTypes.string.isRequired, 44 | [STAGE_AB[1]]: React.PropTypes.string.isRequired, 45 | }).isRequired, 46 | onChange: React.PropTypes.func.isRequired, 47 | }; 48 | 49 | module.exports = StageInput; 50 | -------------------------------------------------------------------------------- /src/script/component/record/kd-graph.jsx: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | 3 | const Util = require('../../util'); 4 | const Chart = Util.getChartClass(); 5 | 6 | class KDGraph extends React.Component { 7 | constructor() { 8 | super(); 9 | 10 | this.chart = null; 11 | } 12 | 13 | componentDidMount() { 14 | const ctx = this.refs.graph.getContext('2d'); 15 | const { 16 | labels, 17 | data, 18 | tooltip, 19 | scaleMax, scaleMin, 20 | } = this.props; 21 | 22 | const cData = { 23 | labels: labels, 24 | datasets: [{ 25 | type: 'line', 26 | data: data, 27 | label: null, 28 | borderColor: '#FF6E00', 29 | borderWidth: 1, 30 | pointRadius: 3, 31 | }, { 32 | type: 'line', 33 | data: data, 34 | label: null, 35 | borderWidth: 0, 36 | pointRadius: 0, 37 | }] 38 | }; 39 | 40 | const cOptions = { 41 | tooltips: { 42 | callbacks: { 43 | title: () => { return ''; }, 44 | label: (item) => { return tooltip[item.index]; } 45 | } 46 | }, 47 | scales: { 48 | xAxes: [{ 49 | ticks: { autoSkip: false, } 50 | }], 51 | yAxes: [{ 52 | gridLines: { color: 'rgba(255, 110, 0, .25)' }, 53 | ticks: { 54 | min: scaleMin, 55 | max: scaleMax, 56 | callback: Util.getKDRatioStr, 57 | stepSize: 1, 58 | } 59 | },{ 60 | position: 'right', 61 | gridLines: { display: false }, 62 | ticks: { 63 | min: scaleMin, 64 | max: scaleMax, 65 | callback: Util.getKDRatioStr, 66 | stepSize: 1, 67 | } 68 | }] 69 | } 70 | }; 71 | 72 | this.chart = new Chart(ctx, { 73 | data: cData, 74 | options: cOptions, 75 | }); 76 | } 77 | 78 | componentWillUnmount() { 79 | this.chart.destroy(); 80 | this.chart = null; 81 | } 82 | 83 | render() { 84 | const { w, h } = Util.getCanvasSize(); 85 | 86 | return ( 87 |
88 |

キル・デスのヒリツ

89 |
90 | 91 |
92 |
93 | ); 94 | } 95 | } 96 | 97 | module.exports = KDGraph; 98 | -------------------------------------------------------------------------------- /src/script/component/record/list.jsx: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | 3 | const { 4 | RULE, 5 | STAGE, 6 | RESULT, 7 | RECORD_LIMIT, 8 | } = require('../../const'); 9 | const Util = require('../../util'); 10 | 11 | 12 | class List extends React.Component { 13 | render() { 14 | const { 15 | records, 16 | modifyRecord, 17 | removeRecord, 18 | } = this.props; 19 | 20 | return ( 21 | 50 | ); 51 | } 52 | } 53 | 54 | module.exports = List; 55 | -------------------------------------------------------------------------------- /src/script/component/record/mod-popup.jsx: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | const assign = require('object-assign'); 3 | 4 | const Util = require('../../util'); 5 | 6 | const RuleInput = require('../input/rule-input.jsx'); 7 | const SingleStageInput = require('../input/single-stage-input.jsx'); 8 | const ResultInput = require('../input/result-input.jsx'); 9 | const ResultOptionInput = require('../input/result-option-input.jsx'); 10 | const RateInput = require('../input/rate-input.jsx'); 11 | const KDInput = require('../input/kd-input.jsx'); 12 | const SaveBtn = require('../input/save-btn.jsx'); 13 | 14 | class ModPopup extends React.Component { 15 | constructor(props) { 16 | super(); 17 | 18 | this.state = assign({}, props.modItem); 19 | 20 | this.onChange = this.onChange.bind(this); 21 | this.onCancel = this.onCancel.bind(this); 22 | this.onSave = this.onSave.bind(this); 23 | } 24 | 25 | onChange(key, val) { 26 | let { rateRank, rateScore } = Util.getRankAndScore(this.state.rate); 27 | if (key === 'rateRank') { 28 | rateRank = val|0; 29 | this.setState({ rate: rateRank + rateScore }); 30 | return; 31 | } 32 | if (key === 'rateScore') { 33 | rateScore = val|0; 34 | this.setState({ rate: rateRank + rateScore }); 35 | return; 36 | } 37 | 38 | this.setState({ [key]: val }); 39 | } 40 | 41 | onCancel() { 42 | this.props.onModify(null); 43 | } 44 | 45 | onSave() { 46 | this.props.onModify(this.state); 47 | } 48 | 49 | render() { 50 | const { 51 | rule, 52 | stage, 53 | result, 54 | tagmatch, 55 | missmatch, 56 | rate, 57 | kill, death, 58 | } = this.state; 59 | const { rateRank, rateScore } = Util.getRankAndScore(rate); 60 | const canInput = Util.canInput(rateScore); 61 | const isDisconnected = Util.isDisconnected(result); 62 | 63 | return ( 64 |
65 |
    66 |
  • 67 | 71 |
  • 72 | 73 |
  • 74 | 78 |
  • 79 | 80 |
  • 81 | 85 |
  • 86 | 87 |
  • 88 | 94 |
  • 95 | 96 |
  • 97 | 103 | 104 | 109 |
  • 110 | 111 |
  • 112 | {}} 115 | onAfterSave={this.onSave} 116 | /> 117 |
  • 118 | 119 |
  • 120 |
    121 | [キャンセル] 122 |
    123 |
  • 124 |
125 |
126 | ); 127 | } 128 | } 129 | 130 | module.exports = ModPopup; 131 | -------------------------------------------------------------------------------- /src/script/component/record/switcher.jsx: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | const { Link } = require('react-router'); 3 | 4 | const Switcher = ({ 5 | isGraph, isList, 6 | }) => { 7 | return ( 8 |
9 | {isGraph 10 | ? グラフ 11 | : グラフ 12 | } 13 |  |  14 | {isList 15 | ? リスト 16 | : リスト 17 | } 18 |
19 | ); 20 | }; 21 | 22 | Switcher.propTypes = { 23 | isGraph: React.PropTypes.bool.isRequired, 24 | isList: React.PropTypes.bool.isRequired, 25 | }; 26 | 27 | module.exports = Switcher; 28 | -------------------------------------------------------------------------------- /src/script/component/record/udemae-graph.jsx: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | 3 | const Util = require('../../util'); 4 | const Chart = Util.getChartClass(); 5 | const { 6 | RULE, 7 | } = require('../../const'); 8 | 9 | class UdemaeGraph extends React.Component { 10 | constructor() { 11 | super(); 12 | 13 | this.chart = null; 14 | } 15 | 16 | componentDidMount() { 17 | const ctx = this.refs.graph.getContext('2d'); 18 | const { 19 | labels, 20 | data, backgroundColor, 21 | tooltip, 22 | scaleMax, scaleMin, 23 | } = this.props; 24 | 25 | const cData = { 26 | labels: labels, 27 | // 右目盛りのために同じデータを2つ渡す 28 | // 片方はLineチャートだが見えないので関係ない 29 | datasets: [{ 30 | type: 'bar', 31 | data: data, 32 | label: null, 33 | backgroundColor: backgroundColor, 34 | }, { 35 | type: 'line', 36 | data: data, 37 | label: null, 38 | }] 39 | }; 40 | 41 | const cOptions = { 42 | tooltips: { 43 | callbacks: { 44 | title: () => { return ''; }, 45 | label: (item) => { return tooltip[item.index]; } 46 | } 47 | }, 48 | scales: { 49 | xAxes: [{ 50 | barPercentage: .8, 51 | categoryPercentage: 1, 52 | ticks: { autoSkip: false, } 53 | }], 54 | yAxes: [{ 55 | gridLines: { color: 'rgba(255, 110, 0, .25)' }, 56 | ticks: { 57 | min: scaleMin, 58 | max: scaleMax, 59 | callback: Util.getRateStr, 60 | autoSkip: false, 61 | } 62 | },{ 63 | position: 'right', 64 | gridLines: { display: false }, 65 | ticks: { 66 | min: scaleMin, 67 | max: scaleMax, 68 | callback: Util.getRateStr, 69 | autoSkip: false, 70 | } 71 | }] 72 | } 73 | }; 74 | 75 | this.chart = new Chart(ctx, { 76 | data: cData, 77 | options: cOptions, 78 | }); 79 | } 80 | 81 | componentWillUnmount() { 82 | this.chart.destroy(); 83 | this.chart = null; 84 | } 85 | 86 | render() { 87 | const { w, h } = Util.getCanvasSize(); 88 | 89 | return ( 90 |
91 |

ウデマエのスイイ

92 |
93 | {Object.keys(RULE).map((key, idx) => { 94 | return ( 95 | 96 | ■{RULE[key]} 97 | 98 | ); 99 | })} 100 |
101 |
102 | 103 |
104 |
105 | ); 106 | } 107 | } 108 | 109 | module.exports = UdemaeGraph; 110 | -------------------------------------------------------------------------------- /src/script/component/stat/recent-stat.jsx: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | 3 | const { 4 | RULE, STAGE, 5 | } = require('../../const'); 6 | 7 | const RecentStat = ({ 8 | avgRate, 9 | winRate, winRateFree, winRateTag, 10 | missmatch, 11 | winStreak, loseStreak, 12 | koWinRate, koLoseRate, 13 | goodRule, badRule, 14 | goodStage, badStage, 15 | kdRatio, 16 | }) => { 17 | return ( 18 |
19 |

チョッキン

20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 |
適正ウデマエ{avgRate}
全体の勝率{winRate}%
野良の勝率{winRateFree}%
タッグの勝率{winRateTag}%
マッチング事故率{missmatch}%
連勝記録{winStreak}連勝
連敗記録{loseStreak}連敗
KO勝ち率{koWinRate}%
KO負け率{koLoseRate}%
キルレ{kdRatio}
勝ってるルール{RULE[goodRule] || '-'}
負けてるルール{RULE[badRule] || '-'}
勝ってるステージ{STAGE[goodStage] || '-'}
負けてるステージ{STAGE[badStage] || '-'}
81 |
82 | ); 83 | }; 84 | 85 | RecentStat.propTypes = { 86 | avgRate: React.PropTypes.string.isRequired, 87 | winRate: React.PropTypes.string.isRequired, 88 | winRateFree: React.PropTypes.string.isRequired, 89 | winRateTag: React.PropTypes.string.isRequired, 90 | missmatch: React.PropTypes.string.isRequired, 91 | winStreak: React.PropTypes.number.isRequired, 92 | loseStreak: React.PropTypes.number.isRequired, 93 | koWinRate: React.PropTypes.string.isRequired, 94 | koLoseRate: React.PropTypes.string.isRequired, 95 | goodRule: React.PropTypes.string.isRequired, 96 | badRule: React.PropTypes.string.isRequired, 97 | goodStage: React.PropTypes.string.isRequired, 98 | badStage: React.PropTypes.string.isRequired, 99 | kdRatio: React.PropTypes.number.isRequired, 100 | }; 101 | 102 | module.exports = RecentStat; 103 | -------------------------------------------------------------------------------- /src/script/component/stat/total-stat.jsx: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | 3 | const TotalStat = ({ 4 | bestRate, totalIdx, 5 | }) => { 6 | return ( 7 |
8 |

ツウサン

9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
最高ウデマエ{bestRate}
バトル回数{totalIdx}回
22 |
23 | ); 24 | }; 25 | 26 | TotalStat.propTypes = { 27 | bestRate: React.PropTypes.string.isRequired, 28 | totalIdx: React.PropTypes.number.isRequired, 29 | }; 30 | 31 | module.exports = TotalStat; 32 | -------------------------------------------------------------------------------- /src/script/component/stat/win-rate-stat.jsx: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | 3 | const { 4 | RULE, STAGE, 5 | } = require('../../const'); 6 | 7 | const WinRateStat = ({ 8 | winRateDetailByRule, 9 | }) => { 10 | return ( 11 |
12 |

ルールべつ

13 | { 14 | winRateDetailByRule.length === 0 15 | ?

まだデータがトウロクされてないぞ!

16 | : null 17 | } 18 | {winRateDetailByRule.map((rule, idx) => { 19 | return ( 20 |
21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | {rule.detail.map((stage, idx) => { 29 | return ( 30 | 31 | 32 | 33 | 34 | 35 | ); 36 | })} 37 | 38 |
{RULE[rule.id]}合計{rule.total}%{rule.count}戦
{STAGE[stage.id]}{stage.winRate}%{stage.count}戦
39 |
40 | ); 41 | })} 42 |
43 | ); 44 | }; 45 | 46 | WinRateStat.propTypes = { 47 | winRateDetailByRule: React.PropTypes.arrayOf(React.PropTypes.object).isRequired, 48 | }; 49 | 50 | module.exports = WinRateStat; 51 | -------------------------------------------------------------------------------- /src/script/const.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 3 | RESULT: { 4 | 1: '勝ち', 5 | 2: '負け', 6 | 3: 'KO勝ち', 7 | 4: 'KO負け', 8 | 5: '回線落ち' 9 | }, 10 | 11 | RESULT_STR: { 12 | WIN: 1, 13 | LOSE: 2, 14 | KO_WIN: 3, 15 | KO_LOSE: 4, 16 | DISCONNECTED: 5 17 | }, 18 | 19 | RULE: { 20 | 1: 'エリア', 21 | 2: 'ヤグラ', 22 | 3: 'ホコ' 23 | }, 24 | 25 | RULE_COLOR: { 26 | 1: '#FF6E00', 27 | 2: '#00B7A4', 28 | 3: '#CA00DF' 29 | }, 30 | 31 | STAGE: { 32 | 1: 'アロワナモール', 33 | 2: 'Bバスパーク', 34 | 3: 'シオノメ油田', 35 | 4: 'デカライン高架下', 36 | 5: 'ハコフグ倉庫', 37 | 6: 'ホッケふ頭', 38 | 7: 'モズク農園', 39 | 8: 'ネギトロ炭鉱', 40 | 9: 'タチウオパーキング', 41 | 10: 'モンガラキャンプ場', 42 | 11: 'ヒラメが丘団地', 43 | 12: 'マサバ海峡大橋', 44 | 13: 'キンメダイ美術館', 45 | 14: 'マヒマヒリゾート&スパ', 46 | 15: 'ショッツル鉱山', 47 | 16: 'アンチョビットゲームズ' 48 | }, 49 | 50 | RATE_TABLE: { 51 | 'S+': 1000, 52 | 'S': 900, 53 | 'A+': 800, 54 | 'A': 700, 55 | 'A-': 600, 56 | 'B+': 500, 57 | 'B': 400, 58 | 'B-': 300, 59 | 'C+': 200, 60 | 'C': 100, 61 | 'C-': 0 62 | }, 63 | 64 | MAX_RATE_STR: 'S+', 65 | MIN_RATE_STR: 'C-', 66 | MIN_RATE_INPUT: 0, 67 | MAX_RATE_INPUT: 99, 68 | 69 | GRAPH_SIZE_TO_SCREEN: { 70 | W: 0.95, 71 | H: 0.6 72 | }, 73 | 74 | RATE_SCALE_GAP: 20, 75 | KD_SCALE_GAP: 1, 76 | 77 | LABEL_UNIT_PC: 5, 78 | LABEL_UNIT_MOBILE: 10, 79 | 80 | RECORD_LIMIT: 200, 81 | 82 | TWITTER_URL: 'http://twitter.com/share?text=' 83 | 84 | }; 85 | -------------------------------------------------------------------------------- /src/script/main.js: -------------------------------------------------------------------------------- 1 | const ReactDOM = require('react-dom'); 2 | const injectTapEventPlugin = require('react-tap-event-plugin'); 3 | const UserModel = require('./model/user').getInstance(); 4 | const Router = require('./router.jsx'); 5 | 6 | window.onerror = (err) => { 7 | console.error(err); 8 | alert('何やらエラーが出たようです。\nごめんなさい・・。'); 9 | }; 10 | 11 | // 何より先に環境チェック 12 | try { 13 | localStorage.setItem('IA_TEST', 'TEST'); 14 | localStorage.removeItem('IA_TEST'); 15 | } catch(e) { 16 | alert('お使いの環境ではご利用いただけません。\nプライベートブラウズはOFFにしてください。'); 17 | } 18 | 19 | const $app = document.getElementById('jsApp'); 20 | const $boot = document.getElementById('jsBootApp'); 21 | 22 | // 最初は見えなくて、ふわっと起動する 23 | $app.classList.add('is-booted'); 24 | 25 | if (UserModel.get('isFirstTime')) { 26 | $boot.addEventListener('click', _boot, false); 27 | } else { 28 | _boot(); 29 | } 30 | 31 | function _boot() { 32 | injectTapEventPlugin(); 33 | 34 | UserModel.set('isFirstTime', false); 35 | ReactDOM.render( 36 | Router, 37 | $app 38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /src/script/model/_base.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | class BaseModel { 4 | constructor(key, def) { 5 | this.key = key || new Error('NO KEY'); 6 | this.data = def || {}; 7 | 8 | this._init(); 9 | } 10 | 11 | _init() { 12 | this._fetch(); 13 | } 14 | 15 | _fetch() { 16 | let data = localStorage.getItem(this.key); 17 | if (data !== null) { 18 | data = JSON.parse(data); 19 | if (Array.isArray(data)) { 20 | this.data['items'] = data; 21 | } else { 22 | this.data = data; 23 | } 24 | } 25 | } 26 | 27 | _save() { 28 | localStorage.setItem(this.key, JSON.stringify(this.data)); 29 | } 30 | 31 | _clear() { 32 | localStorage.removeItem(this.key); 33 | } 34 | 35 | set(prop, value) { 36 | if (typeof prop === 'object' && value === undefined) { 37 | for (let key in prop) { 38 | this.data[key] = prop[key]; 39 | } 40 | } else { 41 | this.data[prop] = value; 42 | } 43 | 44 | this._save(); 45 | } 46 | 47 | get(key) { 48 | return this.data[key]; 49 | } 50 | } 51 | 52 | module.exports = BaseModel; 53 | -------------------------------------------------------------------------------- /src/script/model/record.js: -------------------------------------------------------------------------------- 1 | const Const = require('../const'); 2 | const Util = require('../util'); 3 | const BaseModel = require('./_base'); 4 | 5 | class RecordModel extends BaseModel { 6 | constructor() { 7 | super('IA_RECORD', { 8 | 'items': [] 9 | }); 10 | } 11 | 12 | // といいつつ修正するコ 13 | _preSave(record) { 14 | // 回線落ちならミスマッチではない 15 | let isDisconnected = Util.isDisconnected(record.result); 16 | if (isDisconnected) { record.missmatch = false; } 17 | 18 | // ありえない入力は0にする 19 | let isValidRate = Util.isValidRate(record.rate); 20 | if (!isValidRate) { record.rate = 0; } 21 | 22 | return record; 23 | } 24 | 25 | setRecord(state) { 26 | const record = { 27 | result: state.result|0, 28 | missmatch: state.missmatch|0, 29 | tagmatch: state.tagmatch|0, 30 | rule: state.rule|0, 31 | stage: state[state.stage]|0, 32 | rate: (state.rateRank|0) + (state.rateScore|0), 33 | kill: state.kill|0, 34 | death: state.death|0, 35 | // 登録日はココでいれる 36 | createdAt: Date.now(), 37 | }; 38 | 39 | let items = this.get('items'); 40 | // リスト追加 41 | items.push(this._preSave(record)); 42 | // data.lengthはLIMITを超えないし、超えたら先頭が消える 43 | while (items.length > Const.RECORD_LIMIT) { 44 | items.shift(); 45 | } 46 | 47 | this.set('items', items); 48 | } 49 | 50 | getRecord(idx) { 51 | return this.get('items')[idx]; 52 | } 53 | 54 | updateRecord(idx, state) { 55 | const record = { 56 | result: state.result|0, 57 | missmatch: state.missmatch|0, 58 | tagmatch: state.tagmatch|0, 59 | rule: state.rule|0, 60 | stage: state.stage|0, 61 | rate: state.rate|0, 62 | kill: state.kill|0, 63 | death: state.death|0, 64 | createdAt: state.createdAt, 65 | }; 66 | 67 | let items = this.get('items'); 68 | items.splice(idx, 1, this._preSave(record)); 69 | this.set('items', items); 70 | } 71 | 72 | removeRecord(idx) { 73 | let items = this.get('items'); 74 | items.splice(idx, 1); 75 | this.set('items', items); 76 | } 77 | 78 | getLatestRecord() { 79 | let items = this.get('items'); 80 | return items[items.length - 1]; 81 | } 82 | 83 | clearAllData() { this._clear(); } 84 | } 85 | 86 | let instance = null; 87 | module.exports = { 88 | getInstance: () => { 89 | if (instance === null) { 90 | instance = new RecordModel(); 91 | } 92 | 93 | return instance; 94 | } 95 | }; 96 | -------------------------------------------------------------------------------- /src/script/model/user.js: -------------------------------------------------------------------------------- 1 | const BaseModel = require('./_base'); 2 | 3 | class UserModel extends BaseModel { 4 | constructor() { 5 | super('IA_USER', { 6 | isFirstTime: true, 7 | totalIdx: 0, 8 | bestRate: 0, 9 | }); 10 | } 11 | 12 | setRecord(rate) { 13 | rate = rate|0; 14 | 15 | const curIdx = this.get('totalIdx')|0; 16 | this.set('totalIdx', curIdx + 1); 17 | this.updateBestRate(rate); 18 | } 19 | 20 | updateBestRate(rate) { 21 | rate = rate|0; 22 | 23 | const curRate = this.get('bestRate')|0; 24 | if (rate > curRate) { 25 | this.set({ 26 | bestRate: rate, 27 | }); 28 | } 29 | } 30 | 31 | clearBestRate() { 32 | this.set('bestRate', 0); 33 | } 34 | 35 | clearAllData() { this._clear(); } 36 | } 37 | 38 | let instance = null; 39 | module.exports = { 40 | getInstance: () => { 41 | if (instance === null) { 42 | instance = new UserModel(); 43 | } 44 | 45 | return instance; 46 | } 47 | }; 48 | -------------------------------------------------------------------------------- /src/script/page/_master.jsx: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | const { Link } = require('react-router'); 3 | 4 | const MasterPage = ({ children }) => { 5 | return ( 6 |
7 |

ウデマエア-カイブ

8 |
    9 |
  • 10 | キロク 11 |
  • 12 |
  • 13 | セイセキ 14 |
  • 15 |
  • 16 | トウロク 17 |
  • 18 |
  • 19 | ソノタ 20 |
  • 21 |
22 | {children} 23 |
24 | ); 25 | }; 26 | 27 | MasterPage.propTypes = { 28 | children: React.PropTypes.element.isRequired, 29 | }; 30 | 31 | module.exports = MasterPage; 32 | -------------------------------------------------------------------------------- /src/script/page/input.jsx: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | 3 | const RecordModel = require('../model/record').getInstance(); 4 | const UserModel = require('../model/user').getInstance(); 5 | const Util = require('../util'); 6 | 7 | const RuleInput = require('../component/input/rule-input.jsx'); 8 | const StageInput = require('../component/input/stage-input.jsx'); 9 | const ResultInput = require('../component/input/result-input.jsx'); 10 | const ResultOptionInput = require('../component/input/result-option-input.jsx'); 11 | const RateInput = require('../component/input/rate-input.jsx'); 12 | const KDInput = require('../component/input/kd-input.jsx'); 13 | const SaveBtn = require('../component/input/save-btn.jsx'); 14 | 15 | // 本セッションでの増減はいつだってコレが基準 16 | // 最後にこの画面をロードした(= 前回プレイ時) 17 | const lastLastRate = (RecordModel.getLatestRecord() || {}).rate || 0; 18 | // 本セッションでいじった内容を貯めておく(変更すると生える) 19 | let lastSettings = { 20 | // rule, 21 | // stageA, 22 | // stageB, 23 | // isOptHide, 24 | }; 25 | 26 | class InputPage extends React.Component { 27 | constructor() { 28 | super(); 29 | 30 | // 初期表示用 31 | // 最新の、最後のデータを使う 32 | const latestRecord = RecordModel.getLatestRecord() || {}; 33 | const { rateRank, rateScore, } = Util.getRankAndScore(latestRecord.rate); 34 | 35 | this.state = { 36 | rule: lastSettings.rule || '1', 37 | stageA: lastSettings.stageA || '1', 38 | stageB: lastSettings.stageB || '6', 39 | stage: 'stageA', 40 | result: '1', 41 | missmatch: false, 42 | tagmatch: false, 43 | rateRank: ''+rateRank, 44 | rateScore: '', 45 | _rateScore: ''+rateScore, // 実体は↑で、これはplaceholder用 46 | recentRateGap: Util.getRecentRateGap(latestRecord.rate|0, lastLastRate), 47 | kill: '', 48 | death: '', 49 | // boolなので 50 | isOptHide: 'isOptHide' in lastSettings ? lastSettings.isOptHide : true, 51 | }; 52 | 53 | this.toggleOptInput = this.toggleOptInput.bind(this); 54 | this.onChange = this.onChange.bind(this); 55 | this.onSave = this.onSave.bind(this); 56 | } 57 | 58 | toggleOptInput(ev) { 59 | ev.preventDefault(); 60 | 61 | const val = !this.state.isOptHide; 62 | this.setState({ 63 | isOptHide: val 64 | }); 65 | // このセッション中は設定を維持する 66 | lastSettings.isOptHide = val; 67 | } 68 | 69 | onChange(key, val) { 70 | this.setState({ [key]: val }); 71 | 72 | // このセッション中は設定を維持する 73 | if (key === 'rule' || key === 'stageA' || key === 'stageB') { 74 | lastSettings[key] = val; 75 | } 76 | } 77 | 78 | onSave() { 79 | const { 80 | rateRank, 81 | rateScore, 82 | } = this.state; 83 | const rate = (rateRank|0) + (rateScore|0); 84 | 85 | // まるごと渡して向こうで捌く 86 | RecordModel.setRecord(this.state); 87 | // 通算バトル数 / ベストウデマエも更新 88 | UserModel.setRecord(rate); 89 | 90 | if (Util.isMobile()) { 91 | this.setState({ 92 | rateScore: '', // モバイルでだけ消したい 93 | kill: '', // モバイルでだけ消したい 94 | death: '', // モバイルでだけ消したい 95 | _rateScore: rateScore, 96 | recentRateGap: Util.getRecentRateGap(rate, lastLastRate), 97 | missmatch: false 98 | }); 99 | } else { 100 | this.setState({ 101 | _rateScore: rateScore, 102 | recentRateGap: Util.getRecentRateGap(rate, lastLastRate), 103 | missmatch: false 104 | }); 105 | } 106 | } 107 | 108 | render() { 109 | const { route } = this.props; 110 | const { 111 | rule, 112 | stage, stageA, stageB, 113 | result, 114 | missmatch, tagmatch, 115 | rateRank, rateScore, _rateScore, 116 | recentRateGap, 117 | isOptHide, 118 | kill, death, 119 | } = this.state; 120 | const isDisconnected = Util.isDisconnected(result); 121 | const canInput = Util.canInput(rateScore); 122 | 123 | return ( 124 |
125 |
    126 | 127 |
  • 128 | 132 |
  • 133 | 134 |
  • 135 | 140 |
  • 141 | 142 |
  • 143 | 147 |
  • 148 | 149 |
  • 150 | 156 |
  • 157 | 158 |
  • 159 | [{isOptHide ? '+' : '-'}]オプションを{isOptHide ? 'ひらく' : 'とじる'} 160 |
  • 161 |
  • 162 | 168 | 169 | 174 |
  • 175 |
176 | 177 |
178 | 今回のウデマエ増減: {recentRateGap.ratePfx}{recentRateGap.rateGap} 179 |
180 | 181 | 185 | 186 |
※マッチング事故は、回線落ちで4vs3で負けた時などに目印として使います。
187 |
※今回のウデマエ増減は、ページ再読み込みでリセットされます。
188 |
189 | ); 190 | } 191 | } 192 | 193 | module.exports = InputPage; 194 | -------------------------------------------------------------------------------- /src/script/page/others.jsx: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | 3 | const RecordModel = require('../model/record').getInstance(); 4 | const UserModel = require('../model/user').getInstance(); 5 | 6 | const Util = require('../util'); 7 | 8 | class OthersPage extends React.Component { 9 | render() { 10 | const { route } = this.props; 11 | 12 | return ( 13 |
14 | 15 |

つかいかた

16 |
17 |

バトル後にウデマエトウロクだ!

18 |

データはキロクセイセキから見れるぞ!

19 |
20 | 21 |

これナニ?

22 |
23 |

にわかガチ勢の@leader22が、ウデマエの上下を可視化するために個人的に作ったものだ!

24 |

なんか変な動きするとかこんな機能が欲しいとか、報告すれば良いことがあるかもしれないぞ!

25 |
26 | 27 |

ヒミツヘイキ

28 | 32 | 33 |
34 | ); 35 | } 36 | 37 | onClickResetBestRate(ev) { 38 | ev.preventDefault(); 39 | 40 | if (window.confirm('変更したデータは元に戻せません。\n本当に最高ウデマエをリセットしますか?')) { 41 | UserModel.clearBestRate(); 42 | 43 | Util.reload(); 44 | } 45 | } 46 | 47 | onClickRestart(ev) { 48 | ev.preventDefault(); 49 | 50 | if (window.confirm('削除したデータは元に戻せません。\n本当に全削除しますか?')) { 51 | UserModel.clearAllData(); 52 | RecordModel.clearAllData(); 53 | 54 | Util.reload(); 55 | } 56 | } 57 | } 58 | 59 | module.exports = OthersPage; 60 | -------------------------------------------------------------------------------- /src/script/page/record.jsx: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | 3 | class RecordPage extends React.Component { 4 | render() { 5 | const { 6 | route, 7 | children, 8 | } = this.props; 9 | 10 | return ( 11 |
12 | {children} 13 |
14 | ); 15 | } 16 | } 17 | 18 | module.exports = RecordPage; 19 | -------------------------------------------------------------------------------- /src/script/page/record/graph.jsx: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | 3 | const RecordModel = require('../../model/record').getInstance(); 4 | 5 | const graphReducer = require('../../reducer/record/graph'); 6 | 7 | const Switcher = require('../../component/record/switcher.jsx'); 8 | const UdemaeGraph = require('../../component/record/udemae-graph.jsx'); 9 | const KDGraph = require('../../component/record/kd-graph.jsx'); 10 | 11 | class GraphPage extends React.Component { 12 | constructor() { 13 | super(); 14 | 15 | this.state = graphReducer(RecordModel.get('items')); 16 | } 17 | 18 | render() { 19 | const { route, } = this.props; 20 | const { 21 | noData, 22 | labels, 23 | uData, uBackgroundColor, 24 | uTooltip, 25 | uScaleMax, uScaleMin, 26 | kdData, 27 | kdTooltip, 28 | kdScaleMax, kdScaleMin, 29 | } = this.state; 30 | 31 | if (noData) { 32 | return ( 33 |
34 |
35 |

まだデータがトウロクされてないぞ!

36 |
37 |
38 | ); 39 | } 40 | 41 | return ( 42 |
43 | 44 | 45 | 52 | 53 | 59 |
60 | ); 61 | } 62 | } 63 | 64 | module.exports = GraphPage; 65 | -------------------------------------------------------------------------------- /src/script/page/record/list.jsx: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | const assign = require('object-assign'); 3 | 4 | const RecordModel = require('../../model/record').getInstance(); 5 | const UserModel = require('../../model/user').getInstance(); 6 | 7 | const listReducer = require('../../reducer/record/list'); 8 | 9 | const List = require('../../component/record/list.jsx'); 10 | const Switcher = require('../../component/record/switcher.jsx'); 11 | const ModPopup = require('../../component/record/mod-popup.jsx'); 12 | 13 | class ListPage extends React.Component { 14 | constructor() { 15 | super(); 16 | 17 | this.state = { 18 | records: listReducer(RecordModel.get('items')), 19 | modItem: null, 20 | modIdx: null, 21 | }; 22 | 23 | this.modifyRecord = this.modifyRecord.bind(this); 24 | this.onModify = this.onModify.bind(this); 25 | this.removeRecord = this.removeRecord.bind(this); 26 | } 27 | 28 | modifyRecord(ev, item, idx) { 29 | ev.preventDefault(); 30 | ev.stopPropagation(); 31 | 32 | if (this.state.modItem) { return; } 33 | 34 | this.setState({ 35 | modItem: assign({}, item), 36 | modIdx: idx, 37 | }); 38 | } 39 | 40 | onModify(record) { 41 | if (record) { 42 | RecordModel.updateRecord(this.state.modIdx, record); 43 | UserModel.updateBestRate(record.rate); 44 | } 45 | this.setState({ 46 | records: listReducer(RecordModel.get('items')), 47 | modItem: null, 48 | modIdx: null 49 | }); 50 | } 51 | 52 | removeRecord(ev, idx) { 53 | ev.preventDefault(); 54 | ev.stopPropagation(); 55 | 56 | if (this.state.modItem) { return; } 57 | 58 | RecordModel.removeRecord(idx); 59 | this.setState({ 60 | records: listReducer(RecordModel.get('items')) 61 | }); 62 | } 63 | 64 | render() { 65 | const { route, } = this.props; 66 | const { 67 | records, 68 | modItem, 69 | } = this.state; 70 | 71 | if (records.length === 0) { 72 | return ( 73 |
74 |
75 |

まだデータがトウロクされてないぞ!

76 |
77 |
78 | ); 79 | } 80 | 81 | return ( 82 |
83 | 84 | 85 | 86 | {modItem 87 | ? 91 | : null 92 | } 93 | 94 | 99 | 100 | 101 |
102 | ); 103 | } 104 | } 105 | 106 | module.exports = ListPage; 107 | -------------------------------------------------------------------------------- /src/script/page/stat.jsx: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | const assign = require('object-assign'); 3 | 4 | const RecordModel = require('../model/record').getInstance(); 5 | const UserModel = require('../model/user').getInstance(); 6 | 7 | const statReducer = require('../reducer/stat'); 8 | 9 | const Util = require('../util'); 10 | 11 | const TotalStat = require('../component/stat/total-stat.jsx'); 12 | const RecentStat = require('../component/stat/recent-stat.jsx'); 13 | const WinRateStat = require('../component/stat/win-rate-stat.jsx'); 14 | 15 | const latestRecord = RecordModel.getLatestRecord(); 16 | 17 | class StatPage extends React.Component { 18 | constructor() { 19 | super(); 20 | 21 | this.state = assign( 22 | statReducer(RecordModel.get('items')), 23 | { 24 | bestRate: Util.getRateStr(UserModel.get('bestRate')), 25 | totalIdx: UserModel.get('totalIdx')|0, 26 | } 27 | ); 28 | } 29 | 30 | render() { 31 | const { route } = this.props; 32 | const { 33 | bestRate, totalIdx, 34 | winRate, winRateFree, winRateTag, 35 | missmatch, 36 | winStreak, loseStreak, 37 | koWinRate, koLoseRate, 38 | goodRule, badRule, 39 | goodStage, badStage, 40 | winRateDetailByRule, 41 | kdRatio, 42 | avgRate, 43 | } = this.state; 44 | const tweetUrl = !!latestRecord 45 | ? Util.getTweetUrl(Util.getRateStr(latestRecord.rate), this.state) 46 | : null; 47 | 48 | return ( 49 |
50 | 53 | 54 | 64 | 65 | { 66 | tweetUrl 67 | ? セイセキをツイート! 68 | : null 69 | } 70 | 71 | 72 | 73 |
74 | ); 75 | } 76 | } 77 | 78 | module.exports = StatPage; 79 | -------------------------------------------------------------------------------- /src/script/reducer/record/graph.js: -------------------------------------------------------------------------------- 1 | const Util = require('../../util'); 2 | const { 3 | RULE, 4 | RECORD_LIMIT, 5 | RULE_COLOR, 6 | RATE_SCALE_GAP, 7 | KD_SCALE_GAP, 8 | LABEL_UNIT_PC, 9 | LABEL_UNIT_MOBILE, 10 | } = require('../../const'); 11 | const LABEL_UNIT = Util.isMobile() ? LABEL_UNIT_MOBILE : LABEL_UNIT_PC; 12 | 13 | module.exports = (records) => { 14 | const ret = { 15 | // 共通 16 | noData: false, 17 | labels: [], 18 | // ウデマエ 19 | uData: [], 20 | uTooltip: [], 21 | uBackgroundColor: [], 22 | uScaleMax: 0, 23 | uScaleMin: 0, 24 | // キルレ 25 | kdData: [], 26 | kdTooltip: [], 27 | kdScaleMax: 0, 28 | kdScaleMin: 0, 29 | }; 30 | 31 | if (records.length === 0) { 32 | ret.noData = true; 33 | return ret; 34 | } 35 | 36 | 37 | // 1ループで必要なデータを集める 38 | // グラフの体裁を合わせるため、実データより大枠を優先 39 | for (let i = 0, l = RECORD_LIMIT; i < l; i++) { 40 | const cnt = i + 1; 41 | const item = records[i] || {}; 42 | 43 | ret.labels.push(cnt % LABEL_UNIT === 0 ? cnt : ''); 44 | 45 | ret.uData.push(item.rate || null); 46 | ret.uTooltip.push(`${RULE[item.rule]}: ${Util.getRateStr(item.rate)}`); 47 | ret.uBackgroundColor.push(RULE_COLOR[item.rule]); 48 | 49 | // 0になるとInfinityになっちゃうので注意 50 | let ratio; 51 | // そもそも入力してない 52 | if (!('kill' in item && 'death' in item)) { 53 | ratio = null; 54 | } else { 55 | ratio = Util.calcKDRatio(item); 56 | } 57 | ret.kdData.push(ratio); 58 | ret.kdTooltip.push(`${item.kill}k / ${item.death}d`); 59 | } 60 | 61 | // ループ後に欲しいやつ 62 | ret.uScaleMax = _getUdemaeScaleMax(ret.uData); 63 | ret.uScaleMin = _getUdemaeScaleMin(ret.uData); 64 | ret.kdScaleMax = Math.max.apply(null, ret.kdData) + KD_SCALE_GAP; 65 | ret.kdScaleMin = 0; 66 | 67 | return ret; 68 | }; 69 | 70 | function _getUdemaeScaleMax(data) { 71 | data = data.filter(Boolean); 72 | return Math.ceil(Math.max.apply(null, data) / 10) * 10 + RATE_SCALE_GAP; 73 | } 74 | 75 | function _getUdemaeScaleMin(data) { 76 | data = data.filter(Boolean); 77 | return Math.max( 78 | 0, 79 | Math.floor(Math.min.apply(null, data) / 10) * 10 - RATE_SCALE_GAP 80 | ); 81 | } 82 | -------------------------------------------------------------------------------- /src/script/reducer/record/list.js: -------------------------------------------------------------------------------- 1 | module.exports = (records) => { 2 | return records.slice().reverse(); 3 | }; 4 | -------------------------------------------------------------------------------- /src/script/reducer/stat.js: -------------------------------------------------------------------------------- 1 | const Util = require('../util'); 2 | 3 | module.exports = (records) => { 4 | let recordsLen = records.length; 5 | let winStreakCount = 0; 6 | let loseStreakCount = 0; 7 | let longestWinStreakCount = 0; 8 | let longestLoseStreakCount = 0; 9 | let missmatchCount = 0; 10 | let winCount = 0; 11 | let loseCount = 0; 12 | let koWinCount = 0; 13 | let koLoseCount = 0; 14 | let tagWinCount = 0; 15 | let tagRecordsLen = 0; 16 | let stageStat = {}; 17 | let ruleStat = {}; 18 | let winRateDetailByRule = { 19 | // ルール別 20 | // 1: { 21 | // ステージ別勝利回数 22 | // 1: { t: 3, w: 1 } 23 | // } 24 | }; 25 | let killCount = 0; 26 | let deathCount = 0; 27 | let totalRate = 0; 28 | 29 | // このループで用意できるものは全て用意する 30 | records.forEach(function(item) { 31 | // マッチングがクソなやつも簡単 32 | if (item.missmatch) { missmatchCount++; } 33 | 34 | // タッグマッチの数 35 | if (item.tagmatch) { tagRecordsLen++; } 36 | 37 | // ステージ別の勝ち負け 38 | if (item.stage in stageStat === false) { 39 | stageStat[item.stage] = { w:0, l: 0 }; 40 | } 41 | // ルール別の勝ち負け 42 | if (item.rule in ruleStat === false) { 43 | ruleStat[item.rule] = { w:0, l: 0 }; 44 | } 45 | 46 | // ルール x ステージの勝率を出す 47 | winRateDetailByRule[item.rule] = winRateDetailByRule[item.rule] || {}; 48 | winRateDetailByRule[item.rule][item.stage] = winRateDetailByRule[item.rule][item.stage] || { w: 0, t: 0 }; 49 | winRateDetailByRule[item.rule][item.stage].t++; 50 | 51 | // 勝った 52 | if (Util.isWin(item.result)) { 53 | winCount++; 54 | if (item.tagmatch) { tagWinCount++; } 55 | 56 | stageStat[item.stage].w++; 57 | ruleStat[item.rule].w++; 58 | winRateDetailByRule[item.rule][item.stage].w++; 59 | 60 | winStreakCount++; 61 | loseStreakCount = 0; 62 | } 63 | // 負けた 64 | else { 65 | loseCount++; 66 | 67 | stageStat[item.stage].l++; 68 | ruleStat[item.rule].l++; 69 | 70 | loseStreakCount++; 71 | winStreakCount = 0; 72 | } 73 | 74 | // 連勝と連敗を記録 75 | longestLoseStreakCount = longestLoseStreakCount < loseStreakCount ? loseStreakCount : longestLoseStreakCount; 76 | longestWinStreakCount = longestWinStreakCount < winStreakCount ? winStreakCount : longestWinStreakCount; 77 | // KO勝ちとKO負け 78 | if (item.result === 3) { koWinCount++; } 79 | if (item.result === 4) { koLoseCount++; } 80 | 81 | if ('kill' in item) { killCount += item.kill; } 82 | if ('death' in item) { deathCount += item.death; } 83 | 84 | totalRate+= item.rate; 85 | }); 86 | 87 | // 以下、各ステージと各ルールにおいて、 88 | // 勝率の最高と最低をそれぞれ出す 89 | // 単純に回数で得手不得手はわからないのでこうする 90 | let stageStatResult = _getGoodAndBad(stageStat); 91 | let ruleStatResult = _getGoodAndBad(ruleStat); 92 | 93 | // ルール別ステージ別の勝率 94 | winRateDetailByRule = _getWinRateDetailByRule(winRateDetailByRule); 95 | 96 | return { 97 | winRate: Util.percentage(winCount, recordsLen), 98 | winRateTag: Util.percentage(tagWinCount, tagRecordsLen), 99 | // 全体からタッグ分をひけば、野良の分がわかる 100 | winRateFree: Util.percentage(winCount - tagWinCount, recordsLen - tagRecordsLen), 101 | koWinRate: Util.percentage(koWinCount, recordsLen), 102 | koLoseRate: Util.percentage(koLoseCount, recordsLen), 103 | missmatch: Util.percentage(missmatchCount, recordsLen), 104 | goodStage: stageStatResult.good, 105 | badStage: stageStatResult.bad, 106 | goodRule: ruleStatResult.good, 107 | badRule: ruleStatResult.bad, 108 | winStreak: longestWinStreakCount, 109 | loseStreak: longestLoseStreakCount, 110 | winRateDetailByRule: winRateDetailByRule, 111 | kdRatio: Util.calcKDRatio({ kill: killCount, death: deathCount }), 112 | avgRate: Util.getRateStr(totalRate / recordsLen), 113 | }; 114 | }; 115 | 116 | function _getGoodAndBad(stat) { 117 | let good = 0, 118 | goodName = ''; 119 | let bad = 0, 120 | badName = ''; 121 | let matchCount = 0, 122 | winRate = 0, 123 | loseRate = 0, 124 | item, 125 | key; 126 | 127 | for (key in stat) { 128 | item = stat[key]; 129 | matchCount = item.w + item.l; 130 | winRate = (item.w / matchCount) * 100; 131 | loseRate = (item.l / matchCount) * 100; 132 | 133 | if (good < winRate) { 134 | good = winRate; 135 | goodName = key; 136 | } 137 | if (bad < loseRate) { 138 | bad = loseRate; 139 | badName = key; 140 | } 141 | } 142 | 143 | return { 144 | good: goodName, 145 | bad: badName 146 | }; 147 | } 148 | 149 | function _getWinRateDetailByRule(winRateDetail) { 150 | let key, key2, rule, stage, res, ret = []; 151 | for (key in winRateDetail) { 152 | rule = winRateDetail[key]; 153 | res = { 154 | id: key, 155 | total: 0, 156 | count: 0, 157 | detail: [] 158 | }; 159 | 160 | let total = 0; 161 | let win = 0; 162 | for (key2 in rule) { 163 | stage = rule[key2]; 164 | res.detail.push({ 165 | id: key2, 166 | count: stage.t, 167 | winRate: Util.percentage(stage.w, stage.t) 168 | }); 169 | win += stage.w; 170 | total += stage.t; 171 | } 172 | 173 | // 勝率のいい順にする 174 | res.detail.sort((a, b) => { return (a.winRate|0) > (b.winRate|0) ? -1 : 1; }); 175 | res.total = Util.percentage(win, total); 176 | res.count = total; 177 | 178 | ret.push(res); 179 | } 180 | 181 | return ret; 182 | } 183 | -------------------------------------------------------------------------------- /src/script/router.jsx: -------------------------------------------------------------------------------- 1 | const React = require('react'); // eslint-disable-line no-unused-vars 2 | const { 3 | Router, 4 | Route, 5 | IndexRedirect, 6 | hashHistory, 7 | } = require('react-router'); 8 | 9 | const Master = require('./page/_master.jsx'); 10 | const RecordPage = require('./page/record.jsx'); 11 | const GraphPage = require('./page/record/graph.jsx'); 12 | const ListPage = require('./page/record/list.jsx'); 13 | const StatPage = require('./page/stat.jsx'); 14 | const InputPage = require('./page/input.jsx'); 15 | const OthersPage = require('./page/others.jsx'); 16 | 17 | module.exports = ( 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | ); 32 | -------------------------------------------------------------------------------- /src/script/util.js: -------------------------------------------------------------------------------- 1 | const Chart = require('chart.js'); 2 | 3 | const Const = require('./const'); 4 | const gSizeW = Const.GRAPH_SIZE_TO_SCREEN.W; 5 | const gSizeH = Const.GRAPH_SIZE_TO_SCREEN.H; 6 | 7 | module.exports = { 8 | reload: () => { 9 | setTimeout(() => { location.replace(location.origin); }); 10 | }, 11 | 12 | isMobile: () => { 13 | return 'ontouchstart' in document; 14 | }, 15 | 16 | getChartClass: () => { 17 | Chart.defaults.global.defaultFontColor = '#fff'; 18 | Chart.defaults.global.responsive = false; 19 | Chart.defaults.global.events = ['mousemove', 'touchstart']; 20 | Chart.defaults.global.legend.display = false; 21 | 22 | return Chart; 23 | }, 24 | 25 | getCanvasSize: () => { 26 | const h = window.innerHeight; 27 | const w = window.innerWidth; 28 | 29 | const longSide = h > w ? h : w; 30 | const shortSide = h > w ? w : h; 31 | 32 | return { 33 | w: ((longSide * gSizeW)|0), 34 | h: ((shortSide * gSizeH)|0) 35 | }; 36 | }, 37 | 38 | formatDate: (time) => { 39 | if (!time) { return ''; } 40 | let date = new Date(time); 41 | let YYYY = date.getFullYear(); 42 | let MM = ('0' + (date.getMonth() + 1)).slice(-2); 43 | let DD = ('0' + date.getDate()).slice(-2); 44 | let hh = ('0' + date.getHours()).slice(-2); 45 | let mm = ('0' + date.getMinutes()).slice(-2); 46 | 47 | return `${YYYY}/${MM}/${DD} ${hh}:${mm}`; 48 | }, 49 | 50 | getRateStr: (val) => { 51 | val = val|0; 52 | 53 | let rate = val % 100; 54 | let wait = val - rate; 55 | 56 | let label = ''; 57 | for (let k in Const.RATE_TABLE) { 58 | if (wait === Const.RATE_TABLE[k]) { 59 | label = k; 60 | break; 61 | } 62 | } 63 | 64 | // 現時点で最高のS+99より上の範囲を見る必要が出てくるとコレ 65 | if (label.length === 0) { 66 | // label = Const.MAX_RATE_STR; 67 | // rate = Const.MAX_RATE_INPUT; 68 | // これで S+99 って出せるけど、グラフ的にしっくりこない 69 | // なぜなら範囲が広すぎるとこれが何個も出るからである 70 | return ''; 71 | } 72 | 73 | return label + rate; 74 | }, 75 | 76 | getRankAndScore: (rate) => { 77 | rate = rate|0; 78 | const rateRank = ((rate / 100)|0) * 100; 79 | return { 80 | rateRank: rateRank, 81 | rateScore: rate - rateRank 82 | }; 83 | }, 84 | 85 | isValidRate: (score) => { 86 | let min = Const.RATE_TABLE[Const.MIN_RATE_STR] + Const.MIN_RATE_INPUT; 87 | let max = Const.RATE_TABLE[Const.MAX_RATE_STR] + Const.MAX_RATE_INPUT; 88 | 89 | return min <= score && score <= max; 90 | }, 91 | 92 | percentage: (c, p) => { 93 | if (c === 0 || p === 0) { return '0'; } 94 | return ((c / p) * 100).toFixed(2); 95 | }, 96 | 97 | // 値の入力欄のチェック 98 | canInput: (rateScoreStr) => { 99 | // 自由入力が空のとこだけでも縛る 100 | if (rateScoreStr.length === 0) { 101 | return false; 102 | } 103 | // 0 - 99以外の値は弾く 104 | let score = rateScoreStr|0; 105 | if (score < Const.MIN_RATE_INPUT || Const.MAX_RATE_INPUT < score) { 106 | return false; 107 | } 108 | 109 | return true; 110 | }, 111 | 112 | isDisconnected: (result) => { 113 | result = result|0; 114 | return result === Const.RESULT_STR.DISCONNECTED; 115 | }, 116 | 117 | isWin: (result) => { 118 | result = result|0; 119 | 120 | if (result === Const.RESULT_STR.WIN || 121 | result === Const.RESULT_STR.KO_WIN) { 122 | return true; 123 | } 124 | return false; 125 | }, 126 | 127 | getRecentRateGap: (latestScore, lastScore) => { 128 | const gap = latestScore - lastScore; 129 | const ret = { 130 | ratePfx: '', 131 | rateGap: Math.abs(gap) 132 | }; 133 | 134 | switch (true) { 135 | case gap === 0: 136 | ret.ratePfx = '±'; 137 | break; 138 | case gap > 0: 139 | ret.ratePfx = '+'; 140 | break; 141 | case gap < 0: 142 | ret.ratePfx = '-'; 143 | break; 144 | default: 145 | } 146 | 147 | return ret; 148 | }, 149 | 150 | getKDRatioStr: (val) => { 151 | const vArr = (''+val).split('.'); 152 | if (vArr.length === 1) { 153 | return ('0' + val).slice(-2) + '.0'; 154 | } 155 | return ('0' + vArr[0]).slice(-2) + '.' + vArr[1]; 156 | }, 157 | 158 | calcKDRatio({ kill, death }) { 159 | let ratio = 0; 160 | // 0k0dは1とする 161 | if (kill === 0 && death === 0) { 162 | ratio = 1; 163 | } 164 | // Nk0dはそのまま 165 | else if (death === 0) { 166 | ratio = ((kill * 10)|0) / 10; 167 | } 168 | else { 169 | ratio = ((kill / death * 10)|0) / 10; 170 | } 171 | 172 | return ratio; 173 | }, 174 | 175 | getTweetUrl(rateStr, state) { 176 | const text = [ 177 | `ウデマエが${rateStr}になったぞ!最近の勝率は${state.winRate}%!`, 178 | `${state.totalIdx}戦のキロクで、いまの適正ウデマエは${state.avgRate}だ!`, 179 | `#ウデマエアーカイブ`, 180 | ].join('\n'); 181 | 182 | return Const.TWITTER_URL + encodeURIComponent(text); 183 | } 184 | }; 185 | -------------------------------------------------------------------------------- /src/style/_app.scss: -------------------------------------------------------------------------------- 1 | .view-app { 2 | position: relative; 3 | height: auto; 4 | min-height: 100%; 5 | padding-bottom: 40px; 6 | 7 | .footer { 8 | position: absolute; 9 | bottom: 0; 10 | left: 0; 11 | right: 0; 12 | padding: .5em 0; 13 | text-align: center; 14 | font-size: .8em; 15 | } 16 | 17 | .intro-button { 18 | @extend %button; 19 | margin: 1em auto; 20 | } 21 | 22 | .title { 23 | font-size: 1.2em; 24 | } 25 | 26 | .tab { 27 | text-align: center; 28 | margin: .75em auto; 29 | font-size: 1.3em; 30 | } 31 | 32 | .tab-item { 33 | display: inline-block; 34 | cursor: pointer; 35 | width: 25%; 36 | 37 | & > a { 38 | display: block; 39 | text-decoration: none; 40 | 41 | &:active { 42 | transform: scale(1.1); 43 | } 44 | 45 | &.is-active { 46 | color: #77EF00; 47 | } 48 | } 49 | } 50 | } 51 | 52 | .view-body { 53 | opacity: 0; 54 | transition: opacity .5s ease; 55 | will-change: opacity; 56 | 57 | &.is-booted { 58 | opacity: 1; 59 | } 60 | } 61 | 62 | -------------------------------------------------------------------------------- /src/style/_common.scss: -------------------------------------------------------------------------------- 1 | %button { 2 | display: block; 3 | width: 300px; 4 | max-width: 300px; 5 | line-height: 40px; 6 | height: 40px; 7 | margin: 0 auto; 8 | border: none; 9 | background-color: #FF6E00; 10 | color: #fff; 11 | font-size: 1.3em; 12 | font-weight: bold; 13 | text-decoration: none; 14 | text-align: center; 15 | cursor: pointer; 16 | 17 | &:hover { 18 | opacity: .8; 19 | } 20 | &:active { 21 | transform: translateY(1px); 22 | } 23 | &[disabled] { 24 | cursor: default; 25 | } 26 | } 27 | 28 | %content { 29 | margin: 1em auto; 30 | width: 90%; 31 | } 32 | 33 | @font-face { 34 | font-family: 'ikamodoki'; 35 | src: url('../font/ikamodoki1_0.ttf') format('truetype'); 36 | } 37 | 38 | .ft-ika { 39 | font-family: 'ikamodoki'; 40 | vertical-align: text-bottom; 41 | padding: 0 2px; 42 | } 43 | 44 | .ft-emp { 45 | font-weight: 900; 46 | } 47 | 48 | .fs-s { 49 | font-size: .85em; 50 | } 51 | 52 | .fc-rule-1 { 53 | color: #FF6E00; 54 | } 55 | 56 | .fc-rule-2 { 57 | color: #00B7A4; 58 | } 59 | 60 | .fc-rule-3 { 61 | color: #CA00DF; 62 | } 63 | 64 | .wrap { 65 | padding: .5em; 66 | margin: 1em; 67 | background-color: rgba(0, 0, 0, .5); 68 | border-radius: .5em; 69 | } 70 | 71 | .h3 { 72 | @extend %content; 73 | margin-bottom: 0; 74 | } 75 | 76 | .note { 77 | margin: .25em 1em; 78 | font-size: .5em; 79 | } 80 | 81 | .list { 82 | list-style: square inside; 83 | padding: .25em 1em; 84 | 85 | li+li { margin-top: .5em; } 86 | } 87 | -------------------------------------------------------------------------------- /src/style/_input.scss: -------------------------------------------------------------------------------- 1 | .view-input { 2 | 3 | .input-wrap { 4 | @extend %content; 5 | } 6 | 7 | 8 | .input-item { 9 | text-align: center; 10 | 11 | & + .input-item { 12 | margin-top: .75em; 13 | } 14 | 15 | &.m-title { 16 | font-size: .75em; 17 | text-align: left; 18 | cursor: pointer; 19 | } 20 | } 21 | 22 | input[type=radio], 23 | input[type=checkbox] { 24 | margin-right: .2em; 25 | } 26 | 27 | select { 28 | font-size: 1em; 29 | border-radius: 1px; 30 | border: none; 31 | } 32 | 33 | input[type=number] { 34 | width: 60px; 35 | font-size: 1em; 36 | text-align: center; 37 | padding: .25em .5em; 38 | margin-left: .5em; 39 | border: none; 40 | } 41 | 42 | label { 43 | margin: 0 .2em .5em; 44 | display: inline-block; 45 | } 46 | 47 | .stage-select { 48 | & + .stage-select { margin-top: .5em; } 49 | 50 | select { 51 | margin-left: 1em; 52 | } 53 | } 54 | 55 | .set-button, 56 | .wait-button, 57 | .reaction-button { 58 | @extend %button; 59 | margin: 1em auto; 60 | } 61 | .wait-button { 62 | opacity: .8; 63 | } 64 | .reaction-button { 65 | background-color: #77EF00; 66 | } 67 | 68 | .report { 69 | text-align: center; 70 | font-size: .8em; 71 | margin-bottom: -1em; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/style/_record.scss: -------------------------------------------------------------------------------- 1 | .view-record { 2 | .switcher { 3 | text-align: center; 4 | padding: .25em 0; 5 | } 6 | 7 | .graph-wrap { 8 | padding-top: .5em; 9 | margin: 1em auto; 10 | text-align: center; 11 | overflow-y: auto; 12 | -webkit-overflow-scrolling: touch; 13 | background-color: rgba(0, 0, 0, .5); 14 | } 15 | 16 | .graph-cover { 17 | text-align: center; 18 | padding-top: 60px; 19 | } 20 | 21 | .graph { 22 | position: relative; 23 | } 24 | .graph-legend { 25 | position: absolute; 26 | right: .75em; 27 | top: .5em; 28 | font-size: .75em; 29 | text-align: center; 30 | 31 | > span + span { 32 | margin-left: .5em; 33 | } 34 | } 35 | 36 | .record-list { 37 | @extend %content; 38 | } 39 | .record-list-item { 40 | & + .record-list-item { 41 | margin-top: 1em; 42 | padding-top: 1em; 43 | border-top: 1px solid rgba(255, 255, 255, .8); 44 | } 45 | } 46 | .record-list-item-spacer { 47 | height: .5em; 48 | border: none; 49 | } 50 | 51 | .ctrl-wrap { 52 | font-size: .5em; 53 | text-align: right; 54 | } 55 | .mod-mark, 56 | .del-mark { 57 | cursor: pointer; 58 | text-decoration: underline; 59 | } 60 | 61 | .mod-layer { 62 | position: fixed; 63 | top: 0; 64 | left: 0; 65 | right: 0; 66 | bottom: 0; 67 | z-index: 1; 68 | background-color: rgba(0, 0, 0, .8); 69 | } 70 | 71 | .mod-wrap { 72 | position: absolute; 73 | top: 10%; 74 | left: 0; 75 | right: 0; 76 | text-align: center; 77 | background-color: rgba(0, 0, 0, .8); 78 | 79 | .set-button, 80 | .reaction-button { width: 100%; } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/style/_reset.scss: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | box-sizing: border-box; 5 | } 6 | 7 | html, body { 8 | height: 100%; 9 | background-color: #000; 10 | background-image: url(../img/bg.png); 11 | background-repeat: no-repeat; 12 | background-size: cover; 13 | color: #fff; 14 | } 15 | 16 | html { 17 | line-height: 1.2; 18 | } 19 | 20 | body { 21 | font-family: "Helvetica Neue", Helvetica, Arial, Roboto, "Droid Sans", "メイリオ", Meiryo, "MS Pゴシック", "ヒラギノ角ゴ Pro W3", "Hiragino Kaku Gothic ProN", sans-serif; 22 | } 23 | 24 | h1, h2 { 25 | text-align: center; 26 | } 27 | 28 | h1 { 29 | padding-top: .5em!important; 30 | font-size: 1.8em; 31 | } 32 | 33 | h2 { 34 | margin: .25em auto; 35 | } 36 | 37 | p { 38 | margin: .25em 1em; 39 | } 40 | 41 | a { 42 | color: #fff; 43 | } 44 | 45 | ul { 46 | list-style: none; 47 | } 48 | -------------------------------------------------------------------------------- /src/style/_stat.scss: -------------------------------------------------------------------------------- 1 | .view-stat { 2 | 3 | .user-stat { 4 | @extend %content; 5 | line-height: 1.5em; 6 | 7 | td { width: 50%; } 8 | 9 | td.slim { 10 | width: 25%; 11 | text-align: right; 12 | } 13 | 14 | td + td { 15 | text-indent: .5em; 16 | } 17 | } 18 | 19 | .tweet-button { 20 | @extend %button; 21 | margin: 1em auto; 22 | background-color: #50ABF1; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/style/main.scss: -------------------------------------------------------------------------------- 1 | @import "reset"; 2 | 3 | @import "common"; 4 | 5 | @import 6 | "record", 7 | "stat", 8 | "input", 9 | "app"; 10 | --------------------------------------------------------------------------------