Showing
3 changed files
with
415 additions
and
403 deletions
app/node/model/analysis.js
0 → 100644
| 1 | +const analyzeStats = function(characterInfo, analysisEquipment) { | ||
| 2 | + const jobModel = require('./job'); | ||
| 3 | + const job = jobModel[characterInfo.character.job]; | ||
| 4 | + const jobDefault = jobModel.default; | ||
| 5 | + const weaponConst = require('./weapon')[analysisEquipment.weapon] || 1; | ||
| 6 | + | ||
| 7 | + let rebootDamage = 0; | ||
| 8 | + if (characterInfo.character.server.name.indexOf("리부트") == 0) { | ||
| 9 | + // 리부트, 리부트2 월드 반영 | ||
| 10 | + rebootDamage = parseInt(characterInfo.character.level / 2); | ||
| 11 | + } | ||
| 12 | + | ||
| 13 | + const stats = { | ||
| 14 | + major: { | ||
| 15 | + pure: 0, | ||
| 16 | + percent: analysisEquipment.majorPercent + | ||
| 17 | + job.stats.passive.major.percent + | ||
| 18 | + jobDefault.stats.passive.major.percent, | ||
| 19 | + added: 0 | ||
| 20 | + }, | ||
| 21 | + minor: characterInfo.stats.minor, | ||
| 22 | + damage: { | ||
| 23 | + all: characterInfo.stats.damageHyper + | ||
| 24 | + analysisEquipment.damagePercent + | ||
| 25 | + job.stats.passive.damage.all + | ||
| 26 | + jobDefault.stats.passive.damage.all + | ||
| 27 | + rebootDamage, | ||
| 28 | + boss: characterInfo.stats.bossAttackDamage | ||
| 29 | + }, | ||
| 30 | + finalDamage: job.stats.passive.finalDamage, | ||
| 31 | + criticalDamage: characterInfo.stats.criticalDamage + jobDefault.stats.passive.criticalDamage, | ||
| 32 | + attackPower: { | ||
| 33 | + pure: 0, | ||
| 34 | + percent: analysisEquipment.attackPowerPercent + | ||
| 35 | + job.stats.passive.attackPower.percent | ||
| 36 | + }, | ||
| 37 | + ignoreGuard: characterInfo.stats.ignoreGuard | ||
| 38 | + }; | ||
| 39 | + | ||
| 40 | + stats.major.added = characterInfo.stats.majorHyper + | ||
| 41 | + analysisEquipment.majorArcane + | ||
| 42 | + jobDefault.stats.passive.major.added; | ||
| 43 | + stats.major.pure = (characterInfo.stats.major - stats.major.added) / (1 + stats.major.percent / 100); | ||
| 44 | + | ||
| 45 | + stats.attackPower.pure = characterInfo.stats.statAttackPower * 100 / (characterInfo.stats.major * 4 + stats.minor) / job.jobConst / weaponConst / (1 + stats.attackPower.percent / 100) / (1 + stats.damage.all / 100) / (1 + stats.finalDamage / 100); | ||
| 46 | + | ||
| 47 | + return stats; | ||
| 48 | +} | ||
| 49 | + | ||
| 50 | +const calculateEfficiency = function(stats, job, weapon) { | ||
| 51 | + const efficiency = { | ||
| 52 | + major: { | ||
| 53 | + pure: 1, | ||
| 54 | + percent: 0 | ||
| 55 | + }, | ||
| 56 | + attackPower: { | ||
| 57 | + pure: 0, | ||
| 58 | + percent: 0, | ||
| 59 | + }, | ||
| 60 | + damage: 0, | ||
| 61 | + criticalDamage: 0, | ||
| 62 | + ignoreGuard: 0 | ||
| 63 | + }; | ||
| 64 | + | ||
| 65 | + const defaultPower = calculatePower(stats, job, weapon); | ||
| 66 | + | ||
| 67 | + stats.major.pure += 1; | ||
| 68 | + const majorPure = calculatePower(stats, job, weapon) - defaultPower; | ||
| 69 | + stats.major.pure -= 1; | ||
| 70 | + | ||
| 71 | + if (majorPure == 0) | ||
| 72 | + return efficiency; | ||
| 73 | + | ||
| 74 | + stats.major.percent += 1; | ||
| 75 | + efficiency.major.percent = (calculatePower(stats, job, weapon) - defaultPower) / majorPure; | ||
| 76 | + stats.major.percent -= 1; | ||
| 77 | + | ||
| 78 | + stats.attackPower.pure += 1; | ||
| 79 | + efficiency.attackPower.pure = (calculatePower(stats, job, weapon) - defaultPower) / majorPure; | ||
| 80 | + stats.attackPower.pure -= 1; | ||
| 81 | + | ||
| 82 | + stats.attackPower.percent += 1; | ||
| 83 | + efficiency.attackPower.percent = (calculatePower(stats, job, weapon) - defaultPower) / majorPure; | ||
| 84 | + stats.attackPower.percent -= 1; | ||
| 85 | + | ||
| 86 | + stats.damage.all += 1; | ||
| 87 | + efficiency.damage = (calculatePower(stats, job, weapon) - defaultPower) / majorPure; | ||
| 88 | + stats.damage.all -= 1; | ||
| 89 | + | ||
| 90 | + stats.criticalDamage += 1; | ||
| 91 | + efficiency.criticalDamage = (calculatePower(stats, job, weapon) - defaultPower) / majorPure; | ||
| 92 | + stats.criticalDamage -= 1; | ||
| 93 | + | ||
| 94 | + // 곱연산 | ||
| 95 | + const ignoreGuardSaved = stats.ignoreGuard; | ||
| 96 | + stats.ignoreGuard = (1 - (1 - stats.ignoreGuard / 100) * 0.99) * 100; | ||
| 97 | + efficiency.ignoreGuard = (calculatePower(stats, job, weapon) - defaultPower) / majorPure; | ||
| 98 | + stats.ignoreGuard = ignoreGuardSaved; | ||
| 99 | + | ||
| 100 | + return efficiency; | ||
| 101 | +} | ||
| 102 | + | ||
| 103 | +// 버프 적용 스탯 구하기 | ||
| 104 | +const getBuffStats = function(stats, job) { | ||
| 105 | + const jobModel = require('./job'); | ||
| 106 | + const buff = jobModel[job].stats.active; | ||
| 107 | + const defaultBuff = jobModel.default.stats.active; | ||
| 108 | + | ||
| 109 | + return { | ||
| 110 | + major: { | ||
| 111 | + pure: stats.major.pure + buff.major.pure, | ||
| 112 | + percent: stats.major.percent + buff.major.percent, | ||
| 113 | + added: stats.major.added | ||
| 114 | + }, | ||
| 115 | + minor: stats.minor, | ||
| 116 | + damage: { | ||
| 117 | + all: stats.damage.all + buff.damage.all + defaultBuff.damage.all, | ||
| 118 | + boss: stats.damage.boss + buff.damage.boss + defaultBuff.damage.boss | ||
| 119 | + }, | ||
| 120 | + finalDamage: stats.finalDamage, | ||
| 121 | + criticalDamage: stats.criticalDamage + buff.criticalDamage + defaultBuff.criticalDamage, | ||
| 122 | + attackPower: { | ||
| 123 | + pure: stats.attackPower.pure + buff.attackPower.pure, | ||
| 124 | + percent: stats.attackPower.percent + buff.attackPower.percent + defaultBuff.attackPower.percent | ||
| 125 | + }, | ||
| 126 | + ignoreGuard: (1 - (1 - (stats.ignoreGuard / 100)) * (1 - (buff.ignoreGuard / 100)) * (1 - (defaultBuff.ignoreGuard / 100))) * 100 | ||
| 127 | + }; | ||
| 128 | +} | ||
| 129 | + | ||
| 130 | +// 크리티컬 데미지, 보스 공격력, 방어율 무시를 반영하여 방어율 300% 몬스터 공격시 데미지 산출 값 | ||
| 131 | +const calculatePower = function(stats, job, weapon) { | ||
| 132 | + const jobConst = require('./job')[job].jobConst; | ||
| 133 | + const weaponConst = require('./weapon')[weapon]; | ||
| 134 | + return Math.max( | ||
| 135 | + ( | ||
| 136 | + (stats.major.pure * (1 + stats.major.percent / 100) + stats.major.added) * 4 + | ||
| 137 | + stats.minor | ||
| 138 | + ) * | ||
| 139 | + 0.01 * | ||
| 140 | + (stats.attackPower.pure * (1 + stats.attackPower.percent / 100)) * | ||
| 141 | + jobConst * | ||
| 142 | + weaponConst * | ||
| 143 | + (1 + stats.damage.all / 100 + stats.damage.boss / 100) * | ||
| 144 | + (1 + stats.finalDamage / 100) * | ||
| 145 | + (1.35 + stats.criticalDamage / 100) * | ||
| 146 | + (1 - 3 * (1 - stats.ignoreGuard / 100)), | ||
| 147 | + 1); | ||
| 148 | +} | ||
| 149 | + | ||
| 150 | +module.exports = { | ||
| 151 | + analyzeStats: analyzeStats, | ||
| 152 | + calculateEfficiency: calculateEfficiency, | ||
| 153 | + getBuffStats: getBuffStats, | ||
| 154 | + calculatePower: calculatePower, | ||
| 155 | +} | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
app/node/model/character.js
0 → 100644
| 1 | +axios = require('axios'); | ||
| 2 | + | ||
| 3 | +const crwalCharacterCode = async function(nickname, isReboot = false) { | ||
| 4 | + try { | ||
| 5 | + const resp = await axios.get("https://maplestory.nexon.com/Ranking/World/Total?c=" + encodeURI(nickname) + "&w=" + (isReboot ? "0" : "254")); | ||
| 6 | + | ||
| 7 | + const regex = new RegExp(`<dt><a href=\\"\\/Common\\/Character\\/Detail\\/[^\\?]+?\\?p=(.+?)\\"\\s+target=.+?\\/>${nickname}<\\/a><\\/dt>`); | ||
| 8 | + const regexResult = regex.exec(resp.data); | ||
| 9 | + | ||
| 10 | + if (!regexResult) { | ||
| 11 | + if (isReboot) | ||
| 12 | + return -2; | ||
| 13 | + else | ||
| 14 | + return await crwalCharacterCode(nickname, true); | ||
| 15 | + } | ||
| 16 | + | ||
| 17 | + return regexResult[1]; | ||
| 18 | + } catch (error) { | ||
| 19 | + console.log(error); | ||
| 20 | + return -1; | ||
| 21 | + } | ||
| 22 | +}; | ||
| 23 | + | ||
| 24 | +const getCharacterInfo = async function(nickname, characterCode) { | ||
| 25 | + try { | ||
| 26 | + const resp = await axios.get("https://maplestory.nexon.com/Common/Character/Detail/" + encodeURI(nickname) + "?p=" + characterCode); | ||
| 27 | + | ||
| 28 | + if (resp.data.indexOf("공개하지 않은 정보입니다.") >= 0) { | ||
| 29 | + throw new Error("private_character"); | ||
| 30 | + } | ||
| 31 | + | ||
| 32 | + if (resp.data.indexOf("메이플스토리 게임 점검 중에는 이용하실 수 없습니다.") >= 0) { | ||
| 33 | + throw new Error("game_checking"); | ||
| 34 | + } | ||
| 35 | + | ||
| 36 | + const character = { | ||
| 37 | + nickname: nickname, | ||
| 38 | + characterCode: characterCode, | ||
| 39 | + job: null, | ||
| 40 | + level: null, | ||
| 41 | + avatar: null, | ||
| 42 | + server: { | ||
| 43 | + icon: null, | ||
| 44 | + name: null | ||
| 45 | + }, | ||
| 46 | + majorName: null, | ||
| 47 | + attackPowerName: null | ||
| 48 | + }; | ||
| 49 | + const stats = { | ||
| 50 | + major: 0, | ||
| 51 | + minor: 0, | ||
| 52 | + majorHyper: 0, | ||
| 53 | + damageHyper: 0, | ||
| 54 | + criticalDamage: 0, | ||
| 55 | + bossAttackDamage: 0, | ||
| 56 | + ignoreGuard: 0, | ||
| 57 | + statAttackPower: 0 | ||
| 58 | + }; | ||
| 59 | + | ||
| 60 | + const { JSDOM } = require('jsdom'); | ||
| 61 | + const dom = new JSDOM(resp.data); | ||
| 62 | + const $ = (require('jquery'))(dom.window); | ||
| 63 | + | ||
| 64 | + const jobModel = require('./job'); | ||
| 65 | + const statModel = require('./stat'); | ||
| 66 | + | ||
| 67 | + character.job = $(".tab01_con_wrap .table_style01:eq(0) tbody tr:eq(0) td:eq(1) span").text(); | ||
| 68 | + character.level = parseInt($(".char_info dl:eq(0) dd").text().substring(3)); | ||
| 69 | + character.avatar = $(".char_img img").attr("src"); | ||
| 70 | + character.server = { | ||
| 71 | + name: $(".char_info dl:eq(2) dd").text(), | ||
| 72 | + icon: $(".char_info dl:eq(2) dd img").attr("src") | ||
| 73 | + }; | ||
| 74 | + character.majorName = jobModel[character.job].major; | ||
| 75 | + character.attackPowerName = character.majorName == "INT" ? "마력" : "공격력"; | ||
| 76 | + | ||
| 77 | + const $statInfo = $(".tab01_con_wrap .table_style01:eq(1)"); | ||
| 78 | + $("tbody tr", $statInfo).each(function() { | ||
| 79 | + if ($("th", this).length == 1) { | ||
| 80 | + if ($("th span", this).text() == "하이퍼스탯") { | ||
| 81 | + const values = $("td span", this).html().split("<br>"); | ||
| 82 | + | ||
| 83 | + const regexMajor = new RegExp(`${statModel[character.majorName].korean} (\\d+) 증가`); | ||
| 84 | + const regexDamage = new RegExp(`^데미지 (\\d+)% 증가`); | ||
| 85 | + | ||
| 86 | + let regexResult; | ||
| 87 | + for (let i = 0; i < values.length; i++) { | ||
| 88 | + if (regexResult = regexMajor.exec(values[i])) | ||
| 89 | + stats['majorHyper'] = parseInt(regexResult[1]); | ||
| 90 | + else if (regexResult = regexDamage.exec(values[i])) | ||
| 91 | + stats['damageHyper'] = parseInt(regexResult[1]); | ||
| 92 | + } | ||
| 93 | + } | ||
| 94 | + } else { | ||
| 95 | + for (let i = 0; i < 2; i++) { | ||
| 96 | + const statName = $(`th:eq(${i}) span`, this).text(); | ||
| 97 | + const value = $(`td:eq(${i}) span`, this).text().replace(/\,/g, ""); | ||
| 98 | + | ||
| 99 | + switch (statName) { | ||
| 100 | + case character.majorName: | ||
| 101 | + stats['major'] = parseInt(value); | ||
| 102 | + break; | ||
| 103 | + case jobModel[character.job].minor: | ||
| 104 | + stats['minor'] = parseInt(value); | ||
| 105 | + break; | ||
| 106 | + case "크리티컬 데미지": | ||
| 107 | + stats['criticalDamage'] = parseInt(value); | ||
| 108 | + break; | ||
| 109 | + case "보스공격력": | ||
| 110 | + stats['bossAttackDamage'] = parseInt(value); | ||
| 111 | + break; | ||
| 112 | + case "방어율무시": | ||
| 113 | + stats['ignoreGuard'] = parseInt(value); | ||
| 114 | + break; | ||
| 115 | + case "스탯공격력": | ||
| 116 | + stats['statAttackPower'] = parseInt(value.split(' ~ ')[1]); | ||
| 117 | + } | ||
| 118 | + } | ||
| 119 | + } | ||
| 120 | + }); | ||
| 121 | + | ||
| 122 | + return { | ||
| 123 | + character: character, | ||
| 124 | + stats: stats | ||
| 125 | + }; | ||
| 126 | + } catch (error) { | ||
| 127 | + console.log(error); | ||
| 128 | + if (error.message == "private_character") | ||
| 129 | + return -1; | ||
| 130 | + else if (error.message == "game_checking") | ||
| 131 | + return -2; | ||
| 132 | + else | ||
| 133 | + return -999; | ||
| 134 | + } | ||
| 135 | +} | ||
| 136 | + | ||
| 137 | +const analyzeEquipment = async function(nickname, characterCode, job) { | ||
| 138 | + try { | ||
| 139 | + const resp = await axios.get("https://maplestory.nexon.com/Common/Character/Detail/" + encodeURI(nickname) + "/Equipment?p=" + characterCode); | ||
| 140 | + | ||
| 141 | + if (resp.data.indexOf("공개하지 않은 정보입니다.") >= 0) { | ||
| 142 | + throw new Error("private_character"); | ||
| 143 | + } | ||
| 144 | + | ||
| 145 | + if (resp.data.indexOf("메이플스토리 게임 점검 중에는 이용하실 수 없습니다.") >= 0) { | ||
| 146 | + throw new Error("game_checking"); | ||
| 147 | + } | ||
| 148 | + | ||
| 149 | + const { JSDOM } = require('jsdom'); | ||
| 150 | + const dom = new JSDOM(resp.data); | ||
| 151 | + const $ = (require('jquery'))(dom.window); | ||
| 152 | + | ||
| 153 | + // 아케인심볼 분석 | ||
| 154 | + let majorArcane = 0; | ||
| 155 | + const arcaneURLs = []; | ||
| 156 | + $(".tab03_con_wrap .arcane_weapon_wrap .item_pot li span a").each(async function() { | ||
| 157 | + if (!!$(this).attr("href")) | ||
| 158 | + arcaneURLs.push("https://maplestory.nexon.com" + $(this).attr("href")); | ||
| 159 | + }); | ||
| 160 | + | ||
| 161 | + for (let i = 0; i < arcaneURLs.length; i++) { | ||
| 162 | + const equipmentResp = await axios.get(arcaneURLs[i], { | ||
| 163 | + headers: { | ||
| 164 | + 'X-Requested-With': 'XMLHttpRequest' | ||
| 165 | + } | ||
| 166 | + }); | ||
| 167 | + | ||
| 168 | + const equipmentDom = new JSDOM(equipmentResp.data.view); | ||
| 169 | + const $equipment = (require('jquery'))(equipmentDom.window); | ||
| 170 | + | ||
| 171 | + majorArcane += parseInt($equipment(".stet_info ul li:eq(2) .point_td font:eq(0)").text().substring(1)); | ||
| 172 | + } | ||
| 173 | + | ||
| 174 | + // 장비 분석 | ||
| 175 | + const jobModel = require('./job'); | ||
| 176 | + | ||
| 177 | + let damagePercent = 0; | ||
| 178 | + let majorPercent = 0; | ||
| 179 | + let attackPowerPercent = 0; | ||
| 180 | + let weapon = undefined; | ||
| 181 | + const equipmentURLs = []; | ||
| 182 | + $(".tab01_con_wrap .weapon_wrap .item_pot li span a").each(async function() { | ||
| 183 | + equipmentURLs.push("https://maplestory.nexon.com" + $(this).attr("href")); | ||
| 184 | + }); | ||
| 185 | + | ||
| 186 | + for (let i = 0; i < equipmentURLs.length; i++) { | ||
| 187 | + const equipmentResp = await axios.get(equipmentURLs[i], { | ||
| 188 | + headers: { | ||
| 189 | + 'X-Requested-With': 'XMLHttpRequest' | ||
| 190 | + } | ||
| 191 | + }); | ||
| 192 | + | ||
| 193 | + const equipmentDom = new JSDOM(equipmentResp.data.view); | ||
| 194 | + const $equipment = (require('jquery'))(equipmentDom.window); | ||
| 195 | + | ||
| 196 | + const equipmentType = $equipment(".item_ability .ablilty02:eq(1) .job_name em").text(); | ||
| 197 | + if (equipmentType.indexOf("손무기") >= 0 && equipmentType.indexOf("블레이드") < 0 && equipmentType.indexOf("대검") < 0) { | ||
| 198 | + weapon = equipmentType.split(" (")[0]; | ||
| 199 | + } | ||
| 200 | + | ||
| 201 | + $equipment(".stet_info ul li").each(function() { | ||
| 202 | + const regexMajor1 = new RegExp(`${jobModel[job].major} : \\+(\\d+)%`); | ||
| 203 | + const regexMajor2 = new RegExp(`올스탯 : \\+(\\d+)%`); | ||
| 204 | + const regexAttackPower = (jobModel[job].major == "INT") ? | ||
| 205 | + new RegExp(`마력 : \\+(\\d+)%`) : | ||
| 206 | + new RegExp(`공격력 : \\+(\\d+)%`); | ||
| 207 | + const regexDamage = new RegExp(`^데미지 : \\+(\\d+)%`); | ||
| 208 | + | ||
| 209 | + if ($(this).find(".stet_th span").text() == "올스탯") { | ||
| 210 | + majorPercent += parseInt($(this).find(".point_td font:eq(0)").text().substring(1)); | ||
| 211 | + } else if ($(this).find(".stet_th span").text().indexOf("잠재옵션") >= 0) { | ||
| 212 | + const values = $(this).find(".point_td").html().split("<br>"); | ||
| 213 | + for (let j = 0; j < values.length; j++) { | ||
| 214 | + const value = values[j].trim(); | ||
| 215 | + let regexResult; | ||
| 216 | + | ||
| 217 | + if (regexResult = (regexMajor1.exec(value) || regexMajor2.exec(value))) { | ||
| 218 | + majorPercent += parseInt(regexResult[1]); | ||
| 219 | + } else if (regexResult = regexAttackPower.exec(value)) { | ||
| 220 | + attackPowerPercent += parseInt(regexResult[1]); | ||
| 221 | + } else if (regexResult = regexDamage.exec(value)) { | ||
| 222 | + damagePercent += parseInt(regexResult[1]); | ||
| 223 | + } | ||
| 224 | + } | ||
| 225 | + } | ||
| 226 | + }) | ||
| 227 | + } | ||
| 228 | + | ||
| 229 | + return { | ||
| 230 | + majorArcane: majorArcane, | ||
| 231 | + majorPercent: majorPercent, | ||
| 232 | + attackPowerPercent: attackPowerPercent, | ||
| 233 | + damagePercent: damagePercent, | ||
| 234 | + weapon: weapon | ||
| 235 | + }; | ||
| 236 | + } catch (error) { | ||
| 237 | + console.log(error); | ||
| 238 | + if (error.message == "private_character") | ||
| 239 | + return -1; | ||
| 240 | + else if (error.message == "game_checking") | ||
| 241 | + return -2; | ||
| 242 | + else | ||
| 243 | + return -999; | ||
| 244 | + } | ||
| 245 | +} | ||
| 246 | + | ||
| 247 | +module.exports = { | ||
| 248 | + crwalCharacterCode: crwalCharacterCode, | ||
| 249 | + getCharacterInfo: getCharacterInfo, | ||
| 250 | + analyzeEquipment: analyzeEquipment, | ||
| 251 | +} | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
| 1 | -axios = require('axios'); | 1 | +const characterModel = require('../model/character'); |
| 2 | - | 2 | +const analysisModel = require('../model/analysis'); |
| 3 | -const crwalCharacterCode = async function(nickname, isReboot = false) { | ||
| 4 | - try { | ||
| 5 | - const resp = await axios.get("https://maplestory.nexon.com/Ranking/World/Total?c=" + encodeURI(nickname) + "&w=" + (isReboot ? "0" : "254")); | ||
| 6 | - | ||
| 7 | - const regex = new RegExp(`<dt><a href=\\"\\/Common\\/Character\\/Detail\\/[^\\?]+?\\?p=(.+?)\\"\\s+target=.+?\\/>${nickname}<\\/a><\\/dt>`); | ||
| 8 | - const regexResult = regex.exec(resp.data); | ||
| 9 | - | ||
| 10 | - if (!regexResult) { | ||
| 11 | - if (isReboot) | ||
| 12 | - return -2; | ||
| 13 | - else | ||
| 14 | - return await crwalCharacterCode(nickname, true); | ||
| 15 | - } | ||
| 16 | - | ||
| 17 | - return regexResult[1]; | ||
| 18 | - } catch (error) { | ||
| 19 | - console.log(error); | ||
| 20 | - return -1; | ||
| 21 | - } | ||
| 22 | -} | ||
| 23 | - | ||
| 24 | -const getCharacterInfo = async function(nickname, characterCode) { | ||
| 25 | - try { | ||
| 26 | - const resp = await axios.get("https://maplestory.nexon.com/Common/Character/Detail/" + encodeURI(nickname) + "?p=" + characterCode); | ||
| 27 | - | ||
| 28 | - if (resp.data.indexOf("공개하지 않은 정보입니다.") >= 0) { | ||
| 29 | - throw new Error("private_character"); | ||
| 30 | - } | ||
| 31 | - | ||
| 32 | - if (resp.data.indexOf("메이플스토리 게임 점검 중에는 이용하실 수 없습니다.") >= 0) { | ||
| 33 | - throw new Error("game_checking"); | ||
| 34 | - } | ||
| 35 | - | ||
| 36 | - const character = { | ||
| 37 | - nickname: nickname, | ||
| 38 | - characterCode: characterCode, | ||
| 39 | - job: null, | ||
| 40 | - level: null, | ||
| 41 | - avatar: null, | ||
| 42 | - server: { | ||
| 43 | - icon: null, | ||
| 44 | - name: null | ||
| 45 | - }, | ||
| 46 | - majorName: null, | ||
| 47 | - attackPowerName: null | ||
| 48 | - }; | ||
| 49 | - const stats = { | ||
| 50 | - major: 0, | ||
| 51 | - minor: 0, | ||
| 52 | - majorHyper: 0, | ||
| 53 | - damageHyper: 0, | ||
| 54 | - criticalDamage: 0, | ||
| 55 | - bossAttackDamage: 0, | ||
| 56 | - ignoreGuard: 0, | ||
| 57 | - statAttackPower: 0 | ||
| 58 | - }; | ||
| 59 | - | ||
| 60 | - const { JSDOM } = require('jsdom'); | ||
| 61 | - const dom = new JSDOM(resp.data); | ||
| 62 | - const $ = (require('jquery'))(dom.window); | ||
| 63 | - | ||
| 64 | - const jobModel = require('../model/job'); | ||
| 65 | - const statModel = require('../model/stat'); | ||
| 66 | - | ||
| 67 | - character.job = $(".tab01_con_wrap .table_style01:eq(0) tbody tr:eq(0) td:eq(1) span").text(); | ||
| 68 | - character.level = parseInt($(".char_info dl:eq(0) dd").text().substring(3)); | ||
| 69 | - character.avatar = $(".char_img img").attr("src"); | ||
| 70 | - character.server = { | ||
| 71 | - name: $(".char_info dl:eq(2) dd").text(), | ||
| 72 | - icon: $(".char_info dl:eq(2) dd img").attr("src") | ||
| 73 | - }; | ||
| 74 | - character.majorName = jobModel[character.job].major; | ||
| 75 | - character.attackPowerName = character.majorName == "INT" ? "마력" : "공격력"; | ||
| 76 | - | ||
| 77 | - const $statInfo = $(".tab01_con_wrap .table_style01:eq(1)"); | ||
| 78 | - $("tbody tr", $statInfo).each(function() { | ||
| 79 | - if ($("th", this).length == 1) { | ||
| 80 | - if ($("th span", this).text() == "하이퍼스탯") { | ||
| 81 | - const values = $("td span", this).html().split("<br>"); | ||
| 82 | - | ||
| 83 | - const regexMajor = new RegExp(`${statModel[character.majorName].korean} (\\d+) 증가`); | ||
| 84 | - const regexDamage = new RegExp(`^데미지 (\\d+)% 증가`); | ||
| 85 | - | ||
| 86 | - let regexResult; | ||
| 87 | - for (let i = 0; i < values.length; i++) { | ||
| 88 | - if (regexResult = regexMajor.exec(values[i])) | ||
| 89 | - stats['majorHyper'] = parseInt(regexResult[1]); | ||
| 90 | - else if (regexResult = regexDamage.exec(values[i])) | ||
| 91 | - stats['damageHyper'] = parseInt(regexResult[1]); | ||
| 92 | - } | ||
| 93 | - } | ||
| 94 | - } else { | ||
| 95 | - for (let i = 0; i < 2; i++) { | ||
| 96 | - const statName = $(`th:eq(${i}) span`, this).text(); | ||
| 97 | - const value = $(`td:eq(${i}) span`, this).text().replace(/\,/g, ""); | ||
| 98 | - | ||
| 99 | - switch (statName) { | ||
| 100 | - case character.majorName: | ||
| 101 | - stats['major'] = parseInt(value); | ||
| 102 | - break; | ||
| 103 | - case jobModel[character.job].minor: | ||
| 104 | - stats['minor'] = parseInt(value); | ||
| 105 | - break; | ||
| 106 | - case "크리티컬 데미지": | ||
| 107 | - stats['criticalDamage'] = parseInt(value); | ||
| 108 | - break; | ||
| 109 | - case "보스공격력": | ||
| 110 | - stats['bossAttackDamage'] = parseInt(value); | ||
| 111 | - break; | ||
| 112 | - case "방어율무시": | ||
| 113 | - stats['ignoreGuard'] = parseInt(value); | ||
| 114 | - break; | ||
| 115 | - case "스탯공격력": | ||
| 116 | - stats['statAttackPower'] = parseInt(value.split(' ~ ')[1]); | ||
| 117 | - } | ||
| 118 | - } | ||
| 119 | - } | ||
| 120 | - }); | ||
| 121 | - | ||
| 122 | - return { | ||
| 123 | - character: character, | ||
| 124 | - stats: stats | ||
| 125 | - }; | ||
| 126 | - } catch (error) { | ||
| 127 | - console.log(error); | ||
| 128 | - if (error.message == "private_character") | ||
| 129 | - return -1; | ||
| 130 | - else if (error.message == "game_checking") | ||
| 131 | - return -2; | ||
| 132 | - else | ||
| 133 | - return -999; | ||
| 134 | - } | ||
| 135 | -} | ||
| 136 | - | ||
| 137 | -const analyzeEquipment = async function(nickname, characterCode, job) { | ||
| 138 | - try { | ||
| 139 | - const resp = await axios.get("https://maplestory.nexon.com/Common/Character/Detail/" + encodeURI(nickname) + "/Equipment?p=" + characterCode); | ||
| 140 | - | ||
| 141 | - if (resp.data.indexOf("공개하지 않은 정보입니다.") >= 0) { | ||
| 142 | - throw new Error("private_character"); | ||
| 143 | - } | ||
| 144 | - | ||
| 145 | - if (resp.data.indexOf("메이플스토리 게임 점검 중에는 이용하실 수 없습니다.") >= 0) { | ||
| 146 | - throw new Error("game_checking"); | ||
| 147 | - } | ||
| 148 | - | ||
| 149 | - const { JSDOM } = require('jsdom'); | ||
| 150 | - const dom = new JSDOM(resp.data); | ||
| 151 | - const $ = (require('jquery'))(dom.window); | ||
| 152 | - | ||
| 153 | - // 아케인심볼 분석 | ||
| 154 | - let majorArcane = 0; | ||
| 155 | - const arcaneURLs = []; | ||
| 156 | - $(".tab03_con_wrap .arcane_weapon_wrap .item_pot li span a").each(async function() { | ||
| 157 | - if (!!$(this).attr("href")) | ||
| 158 | - arcaneURLs.push("https://maplestory.nexon.com" + $(this).attr("href")); | ||
| 159 | - }); | ||
| 160 | - | ||
| 161 | - for (let i = 0; i < arcaneURLs.length; i++) { | ||
| 162 | - const equipmentResp = await axios.get(arcaneURLs[i], { | ||
| 163 | - headers: { | ||
| 164 | - 'X-Requested-With': 'XMLHttpRequest' | ||
| 165 | - } | ||
| 166 | - }); | ||
| 167 | - | ||
| 168 | - const equipmentDom = new JSDOM(equipmentResp.data.view); | ||
| 169 | - const $equipment = (require('jquery'))(equipmentDom.window); | ||
| 170 | - | ||
| 171 | - majorArcane += parseInt($equipment(".stet_info ul li:eq(2) .point_td font:eq(0)").text().substring(1)); | ||
| 172 | - } | ||
| 173 | - | ||
| 174 | - // 장비 분석 | ||
| 175 | - const jobModel = require('../model/job'); | ||
| 176 | - | ||
| 177 | - let damagePercent = 0; | ||
| 178 | - let majorPercent = 0; | ||
| 179 | - let attackPowerPercent = 0; | ||
| 180 | - let weapon = undefined; | ||
| 181 | - const equipmentURLs = []; | ||
| 182 | - $(".tab01_con_wrap .weapon_wrap .item_pot li span a").each(async function() { | ||
| 183 | - equipmentURLs.push("https://maplestory.nexon.com" + $(this).attr("href")); | ||
| 184 | - }); | ||
| 185 | - | ||
| 186 | - for (let i = 0; i < equipmentURLs.length; i++) { | ||
| 187 | - const equipmentResp = await axios.get(equipmentURLs[i], { | ||
| 188 | - headers: { | ||
| 189 | - 'X-Requested-With': 'XMLHttpRequest' | ||
| 190 | - } | ||
| 191 | - }); | ||
| 192 | - | ||
| 193 | - const equipmentDom = new JSDOM(equipmentResp.data.view); | ||
| 194 | - const $equipment = (require('jquery'))(equipmentDom.window); | ||
| 195 | - | ||
| 196 | - const equipmentType = $equipment(".item_ability .ablilty02:eq(1) .job_name em").text(); | ||
| 197 | - if (equipmentType.indexOf("손무기") >= 0 && equipmentType.indexOf("블레이드") < 0 && equipmentType.indexOf("대검") < 0) { | ||
| 198 | - weapon = equipmentType.split(" (")[0]; | ||
| 199 | - } | ||
| 200 | - | ||
| 201 | - $equipment(".stet_info ul li").each(function() { | ||
| 202 | - const regexMajor1 = new RegExp(`${jobModel[job].major} : \\+(\\d+)%`); | ||
| 203 | - const regexMajor2 = new RegExp(`올스탯 : \\+(\\d+)%`); | ||
| 204 | - const regexAttackPower = (jobModel[job].major == "INT") ? | ||
| 205 | - new RegExp(`마력 : \\+(\\d+)%`) : | ||
| 206 | - new RegExp(`공격력 : \\+(\\d+)%`); | ||
| 207 | - const regexDamage = new RegExp(`^데미지 : \\+(\\d+)%`); | ||
| 208 | - | ||
| 209 | - if ($(this).find(".stet_th span").text() == "올스탯") { | ||
| 210 | - majorPercent += parseInt($(this).find(".point_td font:eq(0)").text().substring(1)); | ||
| 211 | - } else if ($(this).find(".stet_th span").text().indexOf("잠재옵션") >= 0) { | ||
| 212 | - const values = $(this).find(".point_td").html().split("<br>"); | ||
| 213 | - for (let j = 0; j < values.length; j++) { | ||
| 214 | - const value = values[j].trim(); | ||
| 215 | - let regexResult; | ||
| 216 | - | ||
| 217 | - if (regexResult = (regexMajor1.exec(value) || regexMajor2.exec(value))) { | ||
| 218 | - majorPercent += parseInt(regexResult[1]); | ||
| 219 | - } else if (regexResult = regexAttackPower.exec(value)) { | ||
| 220 | - attackPowerPercent += parseInt(regexResult[1]); | ||
| 221 | - } else if (regexResult = regexDamage.exec(value)) { | ||
| 222 | - damagePercent += parseInt(regexResult[1]); | ||
| 223 | - } | ||
| 224 | - } | ||
| 225 | - } | ||
| 226 | - }) | ||
| 227 | - } | ||
| 228 | - | ||
| 229 | - return { | ||
| 230 | - majorArcane: majorArcane, | ||
| 231 | - majorPercent: majorPercent, | ||
| 232 | - attackPowerPercent: attackPowerPercent, | ||
| 233 | - damagePercent: damagePercent, | ||
| 234 | - weapon: weapon | ||
| 235 | - }; | ||
| 236 | - } catch (error) { | ||
| 237 | - console.log(error); | ||
| 238 | - if (error.message == "private_character") | ||
| 239 | - return -1; | ||
| 240 | - else if (error.message == "game_checking") | ||
| 241 | - return -2; | ||
| 242 | - else | ||
| 243 | - return -999; | ||
| 244 | - } | ||
| 245 | -} | ||
| 246 | - | ||
| 247 | -const analyzeStats = function(characterInfo, analysisEquipment) { | ||
| 248 | - const jobModel = require('../model/job'); | ||
| 249 | - const job = jobModel[characterInfo.character.job]; | ||
| 250 | - const jobDefault = jobModel.default; | ||
| 251 | - const weaponConst = require('../model/weapon')[analysisEquipment.weapon] || 1; | ||
| 252 | - | ||
| 253 | - let rebootDamage = 0; | ||
| 254 | - if (characterInfo.character.server.name.indexOf("리부트") == 0) { | ||
| 255 | - // 리부트, 리부트2 월드 반영 | ||
| 256 | - rebootDamage = parseInt(characterInfo.character.level / 2); | ||
| 257 | - } | ||
| 258 | - | ||
| 259 | - const stats = { | ||
| 260 | - major: { | ||
| 261 | - pure: 0, | ||
| 262 | - percent: analysisEquipment.majorPercent + | ||
| 263 | - job.stats.passive.major.percent + | ||
| 264 | - jobDefault.stats.passive.major.percent, | ||
| 265 | - added: 0 | ||
| 266 | - }, | ||
| 267 | - minor: characterInfo.stats.minor, | ||
| 268 | - damage: { | ||
| 269 | - all: characterInfo.stats.damageHyper + | ||
| 270 | - analysisEquipment.damagePercent + | ||
| 271 | - job.stats.passive.damage.all + | ||
| 272 | - jobDefault.stats.passive.damage.all + | ||
| 273 | - rebootDamage, | ||
| 274 | - boss: characterInfo.stats.bossAttackDamage | ||
| 275 | - }, | ||
| 276 | - finalDamage: job.stats.passive.finalDamage, | ||
| 277 | - criticalDamage: characterInfo.stats.criticalDamage + jobDefault.stats.passive.criticalDamage, | ||
| 278 | - attackPower: { | ||
| 279 | - pure: 0, | ||
| 280 | - percent: analysisEquipment.attackPowerPercent + | ||
| 281 | - job.stats.passive.attackPower.percent | ||
| 282 | - }, | ||
| 283 | - ignoreGuard: characterInfo.stats.ignoreGuard | ||
| 284 | - }; | ||
| 285 | - | ||
| 286 | - stats.major.added = characterInfo.stats.majorHyper + | ||
| 287 | - analysisEquipment.majorArcane + | ||
| 288 | - jobDefault.stats.passive.major.added; | ||
| 289 | - stats.major.pure = (characterInfo.stats.major - stats.major.added) / (1 + stats.major.percent / 100); | ||
| 290 | - | ||
| 291 | - stats.attackPower.pure = characterInfo.stats.statAttackPower * 100 / (characterInfo.stats.major * 4 + stats.minor) / job.jobConst / weaponConst / (1 + stats.attackPower.percent / 100) / (1 + stats.damage.all / 100) / (1 + stats.finalDamage / 100); | ||
| 292 | - | ||
| 293 | - return stats; | ||
| 294 | -} | ||
| 295 | - | ||
| 296 | -const calculateEfficiency = function(stats, job, weapon) { | ||
| 297 | - const efficiency = { | ||
| 298 | - major: { | ||
| 299 | - pure: 1, | ||
| 300 | - percent: 0 | ||
| 301 | - }, | ||
| 302 | - attackPower: { | ||
| 303 | - pure: 0, | ||
| 304 | - percent: 0, | ||
| 305 | - }, | ||
| 306 | - damage: 0, | ||
| 307 | - criticalDamage: 0, | ||
| 308 | - ignoreGuard: 0 | ||
| 309 | - }; | ||
| 310 | - | ||
| 311 | - const defaultPower = calculatePower(stats, job, weapon); | ||
| 312 | - | ||
| 313 | - stats.major.pure += 1; | ||
| 314 | - const majorPure = calculatePower(stats, job, weapon) - defaultPower; | ||
| 315 | - stats.major.pure -= 1; | ||
| 316 | - | ||
| 317 | - if (majorPure == 0) | ||
| 318 | - return efficiency; | ||
| 319 | - | ||
| 320 | - stats.major.percent += 1; | ||
| 321 | - efficiency.major.percent = (calculatePower(stats, job, weapon) - defaultPower) / majorPure; | ||
| 322 | - stats.major.percent -= 1; | ||
| 323 | - | ||
| 324 | - stats.attackPower.pure += 1; | ||
| 325 | - efficiency.attackPower.pure = (calculatePower(stats, job, weapon) - defaultPower) / majorPure; | ||
| 326 | - stats.attackPower.pure -= 1; | ||
| 327 | - | ||
| 328 | - stats.attackPower.percent += 1; | ||
| 329 | - efficiency.attackPower.percent = (calculatePower(stats, job, weapon) - defaultPower) / majorPure; | ||
| 330 | - stats.attackPower.percent -= 1; | ||
| 331 | - | ||
| 332 | - stats.damage.all += 1; | ||
| 333 | - efficiency.damage = (calculatePower(stats, job, weapon) - defaultPower) / majorPure; | ||
| 334 | - stats.damage.all -= 1; | ||
| 335 | - | ||
| 336 | - stats.criticalDamage += 1; | ||
| 337 | - efficiency.criticalDamage = (calculatePower(stats, job, weapon) - defaultPower) / majorPure; | ||
| 338 | - stats.criticalDamage -= 1; | ||
| 339 | - | ||
| 340 | - // 곱연산 | ||
| 341 | - const ignoreGuardSaved = stats.ignoreGuard; | ||
| 342 | - stats.ignoreGuard = (1 - (1 - stats.ignoreGuard / 100) * 0.99) * 100; | ||
| 343 | - efficiency.ignoreGuard = (calculatePower(stats, job, weapon) - defaultPower) / majorPure; | ||
| 344 | - stats.ignoreGuard = ignoreGuardSaved; | ||
| 345 | - | ||
| 346 | - return efficiency; | ||
| 347 | -} | ||
| 348 | - | ||
| 349 | -// 버프 적용 스탯 구하기 | ||
| 350 | -const getBuffStats = function(stats, job) { | ||
| 351 | - const jobModel = require('../model/job'); | ||
| 352 | - const buff = jobModel[job].stats.active; | ||
| 353 | - const defaultBuff = jobModel.default.stats.active; | ||
| 354 | - | ||
| 355 | - return { | ||
| 356 | - major: { | ||
| 357 | - pure: stats.major.pure + buff.major.pure, | ||
| 358 | - percent: stats.major.percent + buff.major.percent, | ||
| 359 | - added: stats.major.added | ||
| 360 | - }, | ||
| 361 | - minor: stats.minor, | ||
| 362 | - damage: { | ||
| 363 | - all: stats.damage.all + buff.damage.all + defaultBuff.damage.all, | ||
| 364 | - boss: stats.damage.boss + buff.damage.boss + defaultBuff.damage.boss | ||
| 365 | - }, | ||
| 366 | - finalDamage: stats.finalDamage, | ||
| 367 | - criticalDamage: stats.criticalDamage + buff.criticalDamage + defaultBuff.criticalDamage, | ||
| 368 | - attackPower: { | ||
| 369 | - pure: stats.attackPower.pure + buff.attackPower.pure, | ||
| 370 | - percent: stats.attackPower.percent + buff.attackPower.percent + defaultBuff.attackPower.percent | ||
| 371 | - }, | ||
| 372 | - ignoreGuard: (1 - (1 - (stats.ignoreGuard / 100)) * (1 - (buff.ignoreGuard / 100)) * (1 - (defaultBuff.ignoreGuard / 100))) * 100 | ||
| 373 | - }; | ||
| 374 | -} | ||
| 375 | - | ||
| 376 | -// 크리티컬 데미지, 보스 공격력, 방어율 무시를 반영하여 방어율 300% 몬스터 공격시 데미지 산출 값 | ||
| 377 | -const calculatePower = function(stats, job, weapon) { | ||
| 378 | - const jobConst = require('../model/job')[job].jobConst; | ||
| 379 | - const weaponConst = require('../model/weapon')[weapon]; | ||
| 380 | - return Math.max( | ||
| 381 | - ( | ||
| 382 | - (stats.major.pure * (1 + stats.major.percent / 100) + stats.major.added) * 4 + | ||
| 383 | - stats.minor | ||
| 384 | - ) * | ||
| 385 | - 0.01 * | ||
| 386 | - (stats.attackPower.pure * (1 + stats.attackPower.percent / 100)) * | ||
| 387 | - jobConst * | ||
| 388 | - weaponConst * | ||
| 389 | - (1 + stats.damage.all / 100 + stats.damage.boss / 100) * | ||
| 390 | - (1 + stats.finalDamage / 100) * | ||
| 391 | - (1.35 + stats.criticalDamage / 100) * | ||
| 392 | - (1 - 3 * (1 - stats.ignoreGuard / 100)), | ||
| 393 | - 1); | ||
| 394 | -} | ||
| 395 | 3 | ||
| 396 | module.exports = { | 4 | module.exports = { |
| 397 | getCharacter: async function(req, res) { | 5 | getCharacter: async function(req, res) { |
| ... | @@ -401,7 +9,7 @@ module.exports = { | ... | @@ -401,7 +9,7 @@ module.exports = { |
| 401 | } | 9 | } |
| 402 | 10 | ||
| 403 | const nickname = req.query.nickname; | 11 | const nickname = req.query.nickname; |
| 404 | - const characterCode = await crwalCharacterCode(req.query.nickname); | 12 | + const characterCode = await characterModel.crwalCharacterCode(req.query.nickname); |
| 405 | 13 | ||
| 406 | if (characterCode == -1) { | 14 | if (characterCode == -1) { |
| 407 | res.status(500).send(); | 15 | res.status(500).send(); |
| ... | @@ -411,7 +19,7 @@ module.exports = { | ... | @@ -411,7 +19,7 @@ module.exports = { |
| 411 | return; | 19 | return; |
| 412 | } | 20 | } |
| 413 | 21 | ||
| 414 | - const characterInfo = await getCharacterInfo(nickname, characterCode); | 22 | + const characterInfo = await characterModel.getCharacterInfo(nickname, characterCode); |
| 415 | if (characterInfo == -1) { | 23 | if (characterInfo == -1) { |
| 416 | // 접근 권한 설정 필요 | 24 | // 접근 권한 설정 필요 |
| 417 | res.status(403).send(); | 25 | res.status(403).send(); |
| ... | @@ -425,7 +33,7 @@ module.exports = { | ... | @@ -425,7 +33,7 @@ module.exports = { |
| 425 | return; | 33 | return; |
| 426 | } | 34 | } |
| 427 | 35 | ||
| 428 | - const analysisEquipment = await analyzeEquipment(nickname, characterCode, characterInfo.character.job); | 36 | + const analysisEquipment = await characterModel.analyzeEquipment(nickname, characterCode, characterInfo.character.job); |
| 429 | if (analysisEquipment == -1) { | 37 | if (analysisEquipment == -1) { |
| 430 | // 접근 권한 설정 필요 | 38 | // 접근 권한 설정 필요 |
| 431 | res.status(403).send(); | 39 | res.status(403).send(); |
| ... | @@ -439,21 +47,19 @@ module.exports = { | ... | @@ -439,21 +47,19 @@ module.exports = { |
| 439 | return; | 47 | return; |
| 440 | } | 48 | } |
| 441 | 49 | ||
| 442 | - const stats = analyzeStats(characterInfo, analysisEquipment); | 50 | + const stats = analysisModel.analyzeStats(characterInfo, analysisEquipment); |
| 443 | - const buffStats = getBuffStats(stats, characterInfo.character.job); | 51 | + const buffStats = analysisModel.getBuffStats(stats, characterInfo.character.job); |
| 444 | - const efficiency = calculateEfficiency(stats, characterInfo.character.job, analysisEquipment.weapon); | ||
| 445 | - const buffEfficiency = calculateEfficiency(buffStats, characterInfo.character.job, analysisEquipment.weapon); | ||
| 446 | 52 | ||
| 447 | const result = { | 53 | const result = { |
| 448 | info: characterInfo.character, | 54 | info: characterInfo.character, |
| 449 | analysis: { | 55 | analysis: { |
| 450 | default: { | 56 | default: { |
| 451 | stats: stats, | 57 | stats: stats, |
| 452 | - efficiency: efficiency | 58 | + efficiency: analysisModel.calculateEfficiency(stats, characterInfo.character.job, analysisEquipment.weapon) |
| 453 | }, | 59 | }, |
| 454 | buff: { | 60 | buff: { |
| 455 | stats: buffStats, | 61 | stats: buffStats, |
| 456 | - efficiency: buffEfficiency | 62 | + efficiency: analysisModel.calculateEfficiency(buffStats, characterInfo.character.job, analysisEquipment.weapon) |
| 457 | } | 63 | } |
| 458 | } | 64 | } |
| 459 | }; | 65 | }; | ... | ... |
-
Please register or login to post a comment