vue.js を覚えるために、vue.js を使って以下の自作のしょぼい FEH 用ツールのUIとデータのバインドを書き直してみました。初めて vue.js を触った際の色々なメモを残しておきます。
vue.js に置き換えることで結果的に何が嬉しかったのか
JavaScript の UIとデータの2方向バインディング(UIを変更すればデータを同期、データを変更すればUIを同期)を勝手にやってくれるので、同期処理を自分で書かなくてよくなり、コードが全体的にシンプルになりました。
vue.js の読み込み
以下のようにCDN版を使えば特別な準備は不要です。
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.13/vue.min.js"></script>
ビューモデルと紐づけるルート要素の定義
vue.js でデータをバインドするにはルート要素を定義する必要があります。データをバインドするルートとして紐づける要素にIDをふって、Vueインスタンスのコンストラクタの el でその要素を指定します。
<div id="app">
</div>
<script>
var vm = new Vue({
el: "#app",
})
</script>
ビューモデルのプロパティを定義
以下のようにVueのコンストラクタのdataにUIに紐づけるデータを渡すことでビューモデルのプロパティを定義します。UI側は、このデータに紐づけたい要素にv-model属性でデータ名を指定すると双方向バインディングが自動的に行われるようになります。
<div id="app">
<input class='numeric' type="number" min="1" max="13" step="1" v-model="numDays"></input>
<input class='numeric' type="number" min="0" max="500" step="1" v-model="restEnergy"></input>
</div>
<script>
var dataObj = {
numDays: 7,
restEnergy: 250,
};
var vm = new Vue({
el: "#app",
data: dataObj,
});
</script>
選択ボックスへのデータのバインド
以下のようにする選択ボックスにデータをバインドできます。v-for属性でrankOptionsプロパティをループして<option>要素のvalueとテキストにプロパティの値をバインドしています。{{}}という構文でプロパティの値を直接参照することができます。この中には加算や関数のコールなどJavaScriptの構文を記述することもできます。
<div id="app">
<select v-model="currentRank">
<option v-for="option in rankOptions" v-bind:value="option">
{{ option }}
</option>
</select>
</div>
<script>
var dataObj = {
currentRank: 21,
rankOptions: [
27,
26,
25,
24,
23,
22,
21,
20,
19,
18,
17,
16,
15,
14,
13,
12,
],
}
var vm = new Vue({
el: "#app",
data: dataObj,
})
</script>
データ更新のフック
Vueインスタンスのコンストラクタの updated に関数を設定することでデータ更新時の処理を設定することができます。
<script>
var dataObj = {
numDays: 7,
restEnergy: 250,
}
var vm = new Vue({
el: "#app",
data: dataObj,
updated: function () {
// 更新後の任意処理
console.log("updated!");
}
})
</script>
コードからデータを更新
コードからデータを更新するにはdataに設定した値を更新するか、Vueインスタンスのプロパティに値を設定します。
<script>
var dataObj = {
numDays: 7,
restEnergy: 250,
};
var vm = new Vue({
el: "#app",
data: dataObj,
updated: function () {
// 更新後の任意処理
console.log("updated!");
}
});
vm.numDays = 6;
vm.restEnergy = 200;
</script>
vue.jsに置き換えた後のツールのソースコード
最後にvue.jsに置き換えた後のツールのソースコード全体を貼っておきます。 そのままコピペすれば動作すると思います。
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.13/vue.min.js"></script>
<style>
.rateUp {
color: blue;
font-weight: bold;
}
.rateDown {
color: red;
font-weight: bold;
}
.energyUp {
color: green;
font-weight: bold;
}
th.result {
text-align: left;
font-size: 12px;
padding: 5px;
}
td.result {
font-size: 12px;
padding: 5px;
}
td.paramTable {
padding-top: 15px;
vertical-align: top;
}
.param {
font-size: 12px;
}
.numeric {
width: 100px;
}
</style>
<div id="app">
<h3>パラメーターの入力</h3>
<p>
以下のパラメーターを変更すると、下のテーブルにシミュレーション結果が反映されます。現在の位階や攻撃時の獲得レートなどをご自身の環境に合わせて変更してください。
</p>
<table border='0' style='border-width: 0px'>
<tr>
<td class='paramTable'>
<label class='param'>残り日数:</label>
</td>
<td class='paramTable'>
<input class='numeric' type="number" min="1" max="13" step="1" v-model="numDays"></input>
</td>
</tr>
<tr>
<td class='paramTable'>
<label class='param'>現在エナジー:<br />(1日目回復分を含む値)</label>
</td>
<td class='paramTable'>
<input class='numeric' type="number" min="0" max="500" step="1" v-model="restEnergy"></input>
<br />
(<label class='param'>水瓶LV</label>
<select v-model="maxEnergy" @change="restEnergy = maxEnergy">
<option v-for="option in aetherAmphoraeLevelToMaxEnergyOptions" v-bind:value="option.value">
{{ option.text }}
</option>
</select>)
</td>
</tr>
<tr>
<td class='paramTable'>
<label class='param'>現在レート:</label>
</td>
<td class='paramTable'>
<input class='numeric' v-model="currentRate"
@change="var newRank = calcRankFromRate(currentRate); if (newRank != currentRank) { currentRank = newRank; }"
type="number" min="4400" max="14040" step="10"></input>
<br />
(<label class='param'>位階</label>
<select v-model="currentRank"
@change="var newRate = calcRateFromRank(currentRank); if (newRate != currentRate) { currentRate = newRate; }">
<option v-for="option in rankOptions" v-bind:value="option">
{{ option }}
</option>
</select>)
</td>
</tr>
<tr>
<td class='paramTable'>
<label class='param'>目標レート:</label>
</td>
<td class='paramTable'>
<input class='numeric' v-model="targetRate"
@change="var newRank = calcRankFromRate(targetRate); if (newRank != targetRank) { targetRank = newRank; }"
type="number" min="4400" max="13700" step="10"></input>
<br />
(<label class='param'>位階</label>
<select v-model="targetRank"
@change="var newRate = calcRateFromRank(targetRank); if (newRate != targetRate) { targetRate = newRate; }">
<option v-for="option in rankOptions" v-bind:value="option">
{{ option }}
</option>
</select>)
</td>
</tr>
<tr>
<td class='paramTable'>
<label class='param'>1回の攻撃獲得レート:</label>
</td>
<td class='paramTable'>
<input class='numeric' v-model="obtainedRate" type="number" min="80" max="200" step="10"></input>
</td>
</tr>
<tr>
<td class='paramTable'>
<label class='param'>1日の防衛失敗レート:</label>
</td>
<td class='paramTable'>
<input class='numeric' v-model="lostRate" type="number" min="0" max="80" step="10"></input>
</td>
</tr>
<tr>
<td class='paramTable'>
<label class='param'>攻撃時のエナジー施設破壊数:</label>
</td>
<td class='paramTable'>
<select class='numeric' v-model="breakEnergy">
<option v-for="option in breakEnergyOptions" v-bind:value="option.value">
{{ option.text }}
</option>
</select>
</td>
</tr>
<tr>
<td class='paramTable'>
<label class='param'>エナジーの泉LV:</label>
</td>
<td class='paramTable'>
<select class='numeric' v-model="suppliedEnergy">
<option v-for="option in suppliedEnergyOptions" v-bind:value="option.value">
{{ option.text }}
</option>
</select>
</td>
</tr>
</table>
<h3>シミュレーション結果</h3>
<p>
<table border="1">
<tr>
<th class='result'>現在レート<br />⇒最終レート</th>
<td class='result'>{{currentRate}}(位階{{calcRankFromRate(currentRate)}})<br />
⇒{{finalRate}}(位階{{calcRankFromRate(finalRate)}})</td>
</tr>
<tr>
<th class='result'>攻撃回数</th>
<td class='result'>{{attackCount}}回</td>
</tr>
<tr>
<th class='result'>残エナジー</th>
<td class='result'>{{finalRestEnergy}}<br />
(最後の1戦までに{{meaninglessSuppliedCount}}個のエナジーを逃してOK)
</td>
</tr>
<tr>
<th class='result'>目標まで失えるレート</th>
<td class='result'>{{convertDefenceLostRateToMessage(defenceLostRate, numDays)}}</td>
</tr>
</table>
</p>
<h3>シミュレーション詳細</h3>
<p>
<div v-html="detailLogs"></div>
</p>
</div>
<script>
var rankToRateTable = [
{ rank: 27, rate: 13400 },
{ rank: 26, rate: 13000 },
{ rank: 25, rate: 12600 },
{ rank: 24, rate: 12200 },
{ rank: 23, rate: 11800 },
{ rank: 22, rate: 11400 },
{ rank: 21, rate: 11000 },
{ rank: 20, rate: 9400 },
{ rank: 19, rate: 8200 },
{ rank: 18, rate: 7200 },
{ rank: 17, rate: 6500 },
{ rank: 16, rate: 6000 },
{ rank: 15, rate: 5600 },
{ rank: 14, rate: 5200 },
{ rank: 13, rate: 4800 },
{ rank: 12, rate: 4400 },
];
var dataObj = {
numDays: 7,
restEnergy: 250,
maxEnergy: 250,
aetherAmphoraeLevelToMaxEnergyOptions: [
{ text: '4', value: 250 },
{ text: '3', value: 200 },
{ text: '2', value: 150 },
{ text: '1', value: 100 },
],
currentRate: 11000,
currentRank: 21,
rankOptions: [
27,
26,
25,
24,
23,
22,
21,
20,
19,
18,
17,
16,
15,
14,
13,
12,
],
targetRate: 13400,
targetRank: 27,
obtainedRate: 160,
lostRate: 0,
breakEnergy: 2,
breakEnergyOptions: [
{ text: '2つ破壊', value: 2 },
{ text: '1つ破壊', value: 1 },
{ text: '破壊しない', value: 0 },
],
suppliedEnergy: 70,
suppliedEnergyOptions: [
{ text: '3', value: 70 },
{ text: '2', value: 60 },
{ text: '1', value: 50 },
],
// 出力プロパティ
finalRate: 0,
attackCount: 0,
finalRestEnergy: 0,
meaninglessSuppliedCount: 0,
defenceLostRate: 0,
detailLogs: '',
}
var vm = new Vue({
el: "#app",
data: dataObj,
updated: function () {
saveSettings();
updateOutputs();
}
})
function saveSettings() {
var settings = [
{ name: "numDays", value: dataObj.numDays },
{ name: "restEnergy", value: dataObj.restEnergy },
{ name: "currentRate", value: dataObj.currentRate },
{ name: "targetRate", value: dataObj.targetRate },
{ name: "obtainedRate", value: dataObj.obtainedRate },
{ name: "lostRate", value: dataObj.lostRate },
{ name: "suppliedEnergy", value: dataObj.suppliedEnergy },
{ name: "maxEnergy", value: dataObj.maxEnergy },
{ name: "breakEnergy", value: dataObj.breakEnergy },
];
settings.forEach(setting => {
document.cookie = setting.name + "=" + setting.value;
});
}
function loadSettings() {
var settings = document.cookie.split(';');
if (settings == null) {
return;
}
for (var settingIndex = 0; settingIndex < settings.length; ++settingIndex) {
var keyValue = settings[settingIndex].split('=');
if (keyValue == null || keyValue.length != 2) {
continue;
}
var name = keyValue[0].trim();
var value = keyValue[1].trim();
switch (name) {
case "numDays": dataObj.numDays = Number(value); break;
case "restEnergy": dataObj.restEnergy = Number(value); break;
case "currentRate": dataObj.currentRate = Number(value); break;
case "targetRate": dataObj.targetRate = Number(value); break;
case "obtainedRate": dataObj.obtainedRate = Number(value); break;
case "lostRate": dataObj.lostRate = Number(value); break;
case "suppliedEnergy": dataObj.suppliedEnergy = Number(value); break;
case "maxEnergy": dataObj.maxEnergy = Number(value); break;
case "breakEnergy": dataObj.breakEnergy = Number(value); break;
default: break;
}
}
}
function calcRankFromRate(rate) {
for (var i = 0; i < rankToRateTable.length; ++i) {
var rankToRate = rankToRateTable[i];
if (rate >= rankToRate.rate) {
return Number(rankToRate.rank);
}
}
return -1;
}
function calcRateFromRank(rank) {
for (var i = 0; i < rankToRateTable.length; ++i) {
var rankToRate = rankToRateTable[i];
if (rankToRate.rank == rank) {
return Number(rankToRate.rate);
}
}
return -1;
}
function getLostEnergy(rate) {
var lostEnergy = Math.ceil((9 + Math.ceil(rate / 100)) / 2);
if (lostEnergy > 50) {
return 50;
}
return lostEnergy;
}
function getSuppliedEnergy(lostEnergy, enemyAetherBreakCount) {
return Math.floor(lostEnergy * enemyAetherBreakCount * 0.1);
}
function createLogObj(message, day, rate, energy) {
var logObj = new Object();
logObj.day = day;
logObj.message = message;
logObj.rate = rate;
logObj.energy = energy;
return logObj;
}
function updateOutputs() {
var initRate = Number(vm.currentRate);
var maxEnergy = Number(vm.maxEnergy);
var energy = Number(vm.restEnergy);
var enemyAetherBreakCount = Number(vm.breakEnergy);
var suppliedEnegyPerDay = Number(vm.suppliedEnergy);
var obtainedRatePerAttack = Number(vm.obtainedRate);
var lostRatePerDay = Number(vm.lostRate);
var numDays = Number(vm.numDays);
var targetRate = Number(vm.targetRate);
var rate = Number(initRate);
var attackCount = 0;
var suppliedEneriesByBreakEnemyAether = [];
var logs = [];
logs.push(createLogObj(
"開始",
1, rate, energy));
var day = 1;
for (var i = 0; i < numDays; ++i) {
day = i + 1;
if (i > 0) {
energy += suppliedEnegyPerDay;
if (energy > maxEnergy) {
energy = maxEnergy;
}
logs.push(createLogObj(
"一日のエナジー回復により<span class='energyUp'>エナジー +" + suppliedEnegyPerDay + "</span>",
day, rate, energy));
}
if (lostRatePerDay > 0) {
rate -= lostRatePerDay;
logs.push(createLogObj(
"防衛失敗により<span class='rateDown'>レート -" + lostRatePerDay + "</span>",
day, rate, energy));
}
while (true) {
var lostEnergy = getLostEnergy(rate);
if (energy - lostEnergy < 0) {
break;
}
energy -= lostEnergy;
if (energy > maxEnergy) {
energy = maxEnergy;
}
rate += obtainedRatePerAttack;
++attackCount;
logs.push(createLogObj(
"エナジー" + lostEnergy + "消費の攻撃により<span class='rateUp'>レート +" + obtainedRatePerAttack + "</span>",
day, rate, energy));
supplied = getSuppliedEnergy(lostEnergy, enemyAetherBreakCount);
for (var j = 0; j < enemyAetherBreakCount; ++j) {
suppliedEneriesByBreakEnemyAether.push(supplied / enemyAetherBreakCount);
}
if (supplied > 0) {
energy += supplied;
if (energy > maxEnergy) {
energy = maxEnergy;
}
logs.push(createLogObj(
"相手エナジー施設破壊により<span class='energyUp'>エナジー +" + supplied + "</span>",
day, rate, energy));
}
}
}
var lastSuppliedEnergy = suppliedEneriesByBreakEnemyAether[suppliedEneriesByBreakEnemyAether.length - 1] * enemyAetherBreakCount;
energy -= lastSuppliedEnergy;
logs.push(createLogObj(
"終了<br/>(最後の攻撃の回復エナジー分は意味がないので除去)",
day, rate, energy));
// 最後の攻撃分は無意味なので先に取り除く
var meaninglessEnery = 0;
var meaninglessSuppliedCount = 0;
for (var i = suppliedEneriesByBreakEnemyAether.length - enemyAetherBreakCount - 1; i >= 0; --i) {
var suppliedEnergy = suppliedEneriesByBreakEnemyAether[i];
meaninglessEnery += suppliedEnergy;
if ((energy - meaninglessEnery) < 0) {
break;
}
++meaninglessSuppliedCount;
}
vm.finalRate = rate;
vm.attackCount = attackCount;
vm.finalRestEnergy = energy;
vm.meaninglessSuppliedCount = meaninglessSuppliedCount;
vm.defenceLostRate = (rate - targetRate);
vm.detailLogs = logsToTable(logs);
}
function convertDefenceLostRateToMessage(defenceLostRate, numDays) {
if (defenceLostRate < 0) {
return (-defenceLostRate) + "足りません";
}
else {
return defenceLostRate + "(1日平均" + Math.floor(defenceLostRate / numDays) + ")";
}
}
function logsToTable(logs) {
var html = "<table border='1'><tr ><th class='result'></th><th class='result'>イベント</th><th class='result'>現在レート</th><th class='result'>残エナジー</th></tr>";
var currentDay = 0;
for (var i = 0; i < logs.length; ++i) {
html += "<tr>";
var logObj = logs[i];
if (currentDay != logObj.day) {
currentDay = logObj.day;
var rowspan = 0;
for (var j = i; j < logs.length; ++j) {
if (logs[j].day != currentDay) {
break;
}
++rowspan;
}
html += "<td class='result' rowspan='" + rowspan + "'>" + logObj.day + "日目</td>";
}
html += "<td class='result'>" + logObj.message + "</td>";
html += "<td class='result'>" + logObj.rate + "</td>";
html += "<td class='result'>" + logObj.energy + "</td>";
html += "</tr>";
}
html += "</table>";
return html;
}
loadSettings();
updateOutputs();
</script>