├── .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 |
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 |
22 | {records.map((item, idx) => {
23 | const vIdx = records.length - idx;
24 | return (
25 | -
26 |
27 | {vIdx}/{RECORD_LIMIT}戦目 - {Util.formatDate(item.createdAt)}
28 |
29 |
30 | {item.tagmatch ? 'タッグマッチ' : '野良ガチマッチ'}
31 | {item.missmatch ? ' (マッチング事故)' : ''}
32 |
33 |
34 |
35 | {RULE[item.rule]} in {STAGE[item.stage]}
36 |
37 |
38 | {RESULT[item.result]} - {Util.getRateStr(item.rate)} [ {item.kill||0}k / {item.death||0}d ]
39 |
40 |
41 |
42 | このキロクを
43 | { modifyRecord(ev, item, vIdx - 1); }}>[シュウセイ]
44 | { removeRecord(ev, vIdx - 1); }}>[サクジョ]
45 |
46 |
47 | );
48 | })}
49 |
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 | {avgRate} |
26 |
27 |
28 | 全体の勝率 |
29 | {winRate}% |
30 |
31 |
32 | 野良の勝率 |
33 | {winRateFree}% |
34 |
35 |
36 | タッグの勝率 |
37 | {winRateTag}% |
38 |
39 |
40 | マッチング事故率 |
41 | {missmatch}% |
42 |
43 |
44 | 連勝記録 |
45 | {winStreak}連勝 |
46 |
47 |
48 | 連敗記録 |
49 | {loseStreak}連敗 |
50 |
51 |
52 | KO勝ち率 |
53 | {koWinRate}% |
54 |
55 |
56 | KO負け率 |
57 | {koLoseRate}% |
58 |
59 |
60 | キルレ |
61 | {kdRatio} |
62 |
63 |
64 | 勝ってるルール |
65 | {RULE[goodRule] || '-'} |
66 |
67 |
68 | 負けてるルール |
69 | {RULE[badRule] || '-'} |
70 |
71 |
72 | 勝ってるステージ |
73 | {STAGE[goodStage] || '-'} |
74 |
75 |
76 | 負けてるステージ |
77 | {STAGE[badStage] || '-'} |
78 |
79 |
80 |
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 | {bestRate} |
15 |
16 |
17 | バトル回数 |
18 | {totalIdx}回 |
19 |
20 |
21 |
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 | {RULE[rule.id]}合計 |
25 | {rule.total}% |
26 | {rule.count}戦 |
27 |
28 | {rule.detail.map((stage, idx) => {
29 | return (
30 |
31 | {STAGE[stage.id]} |
32 | {stage.winRate}% |
33 | {stage.count}戦 |
34 |
35 | );
36 | })}
37 |
38 |
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 |
--------------------------------------------------------------------------------