오윤석

Merge branch 'release' into hotfix

1 -# 메이플스토리 스펙 계산기
...\ No newline at end of file ...\ No newline at end of file
1 +# maplespec.ga
2 +
3 +메이플스토리 스펙업 효율을 계산해주는 웹 어플리케이션입니다.
4 +
5 +[View Demo](https://maplespec.ga)
6 +
7 +* 공개설정이 된 메이플스토리 캐릭터 이름(ex 88분고민한닉, 54분고민한닉, 72분고민한닉)을 입력하여 사용할 수 있습니다.
8 +* 해외에 서버가 있어 분석에 1분정도 시간이 소요됩니다.
9 +
10 +## About The Project
11 +![screenshot](images/screenshot.png)
12 +
13 +본 프로젝트는 메이플스토리 게임의 스펙을 계산하여 어떤 스탯을 올리는 것이 효율적인지를 계산해주는 툴입니다. 닉네임 입력만으로 간단하게 스탯 효율을 계산할 수 있습니다.
14 +
15 +### Built With
16 +* [Docker](https://github.com/docker)
17 +* [Express](https://github.com/expressjs/express)
18 +* [Nginx](https://github.com/nginx/nginx)
19 +* [Svelte](https://github.com/sveltejs/svelte)
20 +
21 +## Getting Started
22 +
23 +### Prerequisites
24 +
25 +* docker
26 +
27 +Docker를 사용하여 구동이 가능합니다. docker-compose가 사용이 가능한 환경이어야 합니다. [설치 안내](https://docs.docker.com/compose/install/)
28 +
29 +### Installation
30 +1. clone the repository
31 +```
32 +git clone http://khuhub.khu.ac.kr/2017104005/oss-maple.git
33 +```
34 +
35 +2. checkout release
36 +```
37 +git checkout release
38 +```
39 +
40 +3. docker on
41 +```
42 +docker-compose up
43 +```
44 +
45 +4. (optional) 80(http) 또는 443(https) 포트로 포워딩
46 +
47 +포워딩하지 않은 경우 8081 포트로 프로젝트가 실행됩니다.
48 +
49 +## Contributing
50 +
51 +프로젝트에 기여하고 싶으신 분들은 아래 절차를 따라주시기 바랍니다.
52 +
53 +1. 프로젝트 fork
54 +2. feature branch 생성 (`git checkout -b feature/n-name`) (프로젝트 feature를 구분하기 위해 feature name 앞에 숫자를 넣습니다.)
55 +3. commit (`git commit -m "Add feature`)
56 +4. push (`git push origin feature/n-name`)
57 +5. pull request 생성
58 +
59 +본 프로젝트는 기여를 환영합니다.
60 +
61 +## Contact
62 +
63 +* 오윤석, dhdbstjr98@khu.ac.kr
64 +* 오윤석, admin@com1.kr
...\ No newline at end of file ...\ No newline at end of file
......
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
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) {
4 - try {
5 - const resp = await axios.get("https://maplestory.nexon.com/Ranking/World/Total?c=" + encodeURI(nickname));
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 - return -2;
12 -
13 - return regexResult[1];
14 - } catch (error) {
15 - console.log(error);
16 - return -1;
17 - }
18 -}
19 -
20 -const getCharacterInfo = async function(nickname, characterCode) {
21 - try {
22 - const resp = await axios.get("https://maplestory.nexon.com/Common/Character/Detail/" + encodeURI(nickname) + "?p=" + characterCode);
23 -
24 - if (resp.data.indexOf("공개하지 않은 정보입니다.") >= 0) {
25 - throw new Error("private_character");
26 - }
27 -
28 - if (resp.data.indexOf("메이플스토리 게임 점검 중에는 이용하실 수 없습니다.") >= 0) {
29 - throw new Error("game_checking");
30 - }
31 -
32 - const character = {
33 - nickname: nickname,
34 - characterCode: characterCode,
35 - job: null,
36 - level: null,
37 - avatar: null,
38 - server: {
39 - icon: null,
40 - name: null
41 - },
42 - majorName: null,
43 - attackPowerName: null
44 - };
45 - const stats = {
46 - major: 0,
47 - minor: 0,
48 - majorHyper: 0,
49 - damageHyper: 0,
50 - criticalDamage: 0,
51 - bossAttackDamage: 0,
52 - ignoreGuard: 0,
53 - statAttackPower: 0
54 - };
55 -
56 - const { JSDOM } = require('jsdom');
57 - const dom = new JSDOM(resp.data);
58 - const $ = (require('jquery'))(dom.window);
59 -
60 - const jobModel = require('../model/job');
61 - const statModel = require('../model/stat');
62 -
63 - character.job = $(".tab01_con_wrap .table_style01:eq(0) tbody tr:eq(0) td:eq(1) span").text();
64 - character.level = parseInt($(".char_info dl:eq(0) dd").text().substring(3));
65 - character.avatar = $(".char_img img").attr("src");
66 - character.server = {
67 - name: $(".char_info dl:eq(2) dd").text(),
68 - icon: $(".char_info dl:eq(2) dd img").attr("src")
69 - };
70 - character.majorName = jobModel[character.job].major;
71 - character.attackPowerName = character.majorName == "INT" ? "마력" : "공격력";
72 -
73 - const $statInfo = $(".tab01_con_wrap .table_style01:eq(1)");
74 - $("tbody tr", $statInfo).each(function() {
75 - if ($("th", this).length == 1) {
76 - if ($("th span", this).text() == "하이퍼스탯") {
77 - const values = $("td span", this).html().split("<br>");
78 -
79 - const regexMajor = new RegExp(`${statModel[character.majorName].korean} (\\d+) 증가`);
80 - const regexDamage = new RegExp(`^데미지 (\\d+)% 증가`);
81 -
82 - let regexResult;
83 - for (let i = 0; i < values.length; i++) {
84 - if (regexResult = regexMajor.exec(values[i]))
85 - stats['majorHyper'] = parseInt(regexResult[1]);
86 - else if (regexResult = regexDamage.exec(values[i]))
87 - stats['damageHyper'] = parseInt(regexResult[1]);
88 - }
89 - }
90 - } else {
91 - for (let i = 0; i < 2; i++) {
92 - const statName = $(`th:eq(${i}) span`, this).text();
93 - const value = $(`td:eq(${i}) span`, this).text().replace(/\,/g, "");
94 -
95 - switch (statName) {
96 - case character.majorName:
97 - stats['major'] = parseInt(value);
98 - break;
99 - case jobModel[character.job].minor:
100 - stats['minor'] = parseInt(value);
101 - break;
102 - case "크리티컬 데미지":
103 - stats['criticalDamage'] = parseInt(value);
104 - break;
105 - case "보스공격력":
106 - stats['bossAttackDamage'] = parseInt(value);
107 - break;
108 - case "방어율무시":
109 - stats['ignoreGuard'] = parseInt(value);
110 - break;
111 - case "스탯공격력":
112 - stats['statAttackPower'] = parseInt(value.split(' ~ ')[1]);
113 - }
114 - }
115 - }
116 - });
117 -
118 - return {
119 - character: character,
120 - stats: stats
121 - };
122 - } catch (error) {
123 - console.log(error);
124 - if (error.message == "private_character")
125 - return -1;
126 - else if (error.message == "game_checking")
127 - return -2;
128 - else
129 - return -999;
130 - }
131 -}
132 -
133 -const analyzeEquipment = async function(nickname, characterCode, job) {
134 - try {
135 - const resp = await axios.get("https://maplestory.nexon.com/Common/Character/Detail/" + encodeURI(nickname) + "/Equipment?p=" + characterCode);
136 -
137 - if (resp.data.indexOf("공개하지 않은 정보입니다.") >= 0) {
138 - throw new Error("private_character");
139 - }
140 -
141 - if (resp.data.indexOf("메이플스토리 게임 점검 중에는 이용하실 수 없습니다.") >= 0) {
142 - throw new Error("game_checking");
143 - }
144 -
145 - const { JSDOM } = require('jsdom');
146 - const dom = new JSDOM(resp.data);
147 - const $ = (require('jquery'))(dom.window);
148 -
149 - // 아케인심볼 분석
150 - let majorArcane = 0;
151 - const arcaneURLs = [];
152 - $(".tab03_con_wrap .arcane_weapon_wrap .item_pot li span a").each(async function() {
153 - if (!!$(this).attr("href"))
154 - arcaneURLs.push("https://maplestory.nexon.com" + $(this).attr("href"));
155 - });
156 -
157 - for (let i = 0; i < arcaneURLs.length; i++) {
158 - const equipmentResp = await axios.get(arcaneURLs[i], {
159 - headers: {
160 - 'X-Requested-With': 'XMLHttpRequest'
161 - }
162 - });
163 -
164 - const equipmentDom = new JSDOM(equipmentResp.data.view);
165 - const $equipment = (require('jquery'))(equipmentDom.window);
166 -
167 - majorArcane += parseInt($equipment(".stet_info ul li:eq(2) .point_td font:eq(0)").text().substring(1));
168 - }
169 -
170 - // 장비 분석
171 - const jobModel = require('../model/job');
172 -
173 - let damagePercent = 0;
174 - let majorPercent = 0;
175 - let attackPowerPercent = 0;
176 - let weapon = undefined;
177 - const equipmentURLs = [];
178 - $(".tab01_con_wrap .weapon_wrap .item_pot li span a").each(async function() {
179 - equipmentURLs.push("https://maplestory.nexon.com" + $(this).attr("href"));
180 - });
181 -
182 - for (let i = 0; i < equipmentURLs.length; i++) {
183 - const equipmentResp = await axios.get(equipmentURLs[i], {
184 - headers: {
185 - 'X-Requested-With': 'XMLHttpRequest'
186 - }
187 - });
188 -
189 - const equipmentDom = new JSDOM(equipmentResp.data.view);
190 - const $equipment = (require('jquery'))(equipmentDom.window);
191 -
192 - const equipmentType = $equipment(".item_ability .ablilty02:eq(1) .job_name em").text();
193 - if (equipmentType.indexOf("손무기") >= 0 && equipmentType.indexOf("블레이드") < 0 && equipmentType.indexOf("대검") < 0) {
194 - weapon = equipmentType.split(" (")[0];
195 - }
196 -
197 - $equipment(".stet_info ul li").each(function() {
198 - const regexMajor1 = new RegExp(`${jobModel[job].major} : \\+(\\d+)%`);
199 - const regexMajor2 = new RegExp(`올스탯 : \\+(\\d+)%`);
200 - const regexAttackPower = (jobModel[job].major == "INT") ?
201 - new RegExp(`마력 : \\+(\\d+)%`) :
202 - new RegExp(`공격력 : \\+(\\d+)%`);
203 - const regexDamage = new RegExp(`^데미지 : \\+(\\d+)%`);
204 -
205 - if ($(this).find(".stet_th span").text() == "올스탯") {
206 - majorPercent += parseInt($(this).find(".point_td font:eq(0)").text().substring(1));
207 - } else if ($(this).find(".stet_th span").text().indexOf("잠재옵션") >= 0) {
208 - const values = $(this).find(".point_td").html().split("<br>");
209 - for (let j = 0; j < values.length; j++) {
210 - const value = values[j].trim();
211 - let regexResult;
212 -
213 - if (regexResult = (regexMajor1.exec(value) || regexMajor2.exec(value))) {
214 - majorPercent += parseInt(regexResult[1]);
215 - } else if (regexResult = regexAttackPower.exec(value)) {
216 - attackPowerPercent += parseInt(regexResult[1]);
217 - } else if (regexResult = regexDamage.exec(value)) {
218 - damagePercent += parseInt(regexResult[1]);
219 - }
220 - }
221 - }
222 - })
223 - }
224 -
225 - return {
226 - majorArcane: majorArcane,
227 - majorPercent: majorPercent,
228 - attackPowerPercent: attackPowerPercent,
229 - damagePercent: damagePercent,
230 - weapon: weapon
231 - };
232 - } catch (error) {
233 - console.log(error);
234 - if (error.message == "private_character")
235 - return -1;
236 - else if (error.message == "game_checking")
237 - return -2;
238 - else
239 - return -999;
240 - }
241 -}
242 -
243 -const analyzeStats = function(characterInfo, analysisEquipment) {
244 - const jobModel = require('../model/job');
245 - const job = jobModel[characterInfo.character.job];
246 - const jobDefault = jobModel.default;
247 - const weaponConst = require('../model/weapon')[analysisEquipment.weapon] || 1;
248 - const stats = {
249 - major: {
250 - pure: 0,
251 - percent: analysisEquipment.majorPercent +
252 - job.stats.passive.major.percent +
253 - jobDefault.stats.passive.major.percent,
254 - added: 0
255 - },
256 - minor: characterInfo.stats.minor,
257 - damage: {
258 - all: characterInfo.stats.damageHyper +
259 - analysisEquipment.damagePercent +
260 - job.stats.passive.damage.all +
261 - jobDefault.stats.passive.damage.all,
262 - boss: characterInfo.stats.bossAttackDamage
263 - },
264 - finalDamage: job.stats.passive.finalDamage,
265 - criticalDamage: characterInfo.stats.criticalDamage + jobDefault.stats.passive.criticalDamage,
266 - attackPower: {
267 - pure: 0,
268 - percent: analysisEquipment.attackPowerPercent +
269 - job.stats.passive.attackPower.percent
270 - },
271 - ignoreGuard: characterInfo.stats.ignoreGuard
272 - };
273 -
274 - stats.major.added = characterInfo.stats.majorHyper +
275 - analysisEquipment.majorArcane +
276 - jobDefault.stats.passive.major.added;
277 - stats.major.pure = (characterInfo.stats.major - stats.major.added) / (1 + stats.major.percent / 100);
278 -
279 - 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);
280 -
281 - return stats;
282 -}
283 -
284 -const calculateEfficiency = function(stats, job, weapon) {
285 - const efficiency = {
286 - major: {
287 - pure: 1,
288 - percent: 0
289 - },
290 - attackPower: {
291 - pure: 0,
292 - percent: 0,
293 - },
294 - damage: 0,
295 - criticalDamage: 0,
296 - ignoreGuard: 0
297 - };
298 -
299 - const defaultPower = calculatePower(stats, job, weapon);
300 -
301 - stats.major.pure += 1;
302 - const majorPure = calculatePower(stats, job, weapon) - defaultPower;
303 - stats.major.pure -= 1;
304 -
305 - if (majorPure == 0)
306 - return efficiency;
307 -
308 - stats.major.percent += 1;
309 - efficiency.major.percent = (calculatePower(stats, job, weapon) - defaultPower) / majorPure;
310 - stats.major.percent -= 1;
311 -
312 - stats.attackPower.pure += 1;
313 - efficiency.attackPower.pure = (calculatePower(stats, job, weapon) - defaultPower) / majorPure;
314 - stats.attackPower.pure -= 1;
315 -
316 - stats.attackPower.percent += 1;
317 - efficiency.attackPower.percent = (calculatePower(stats, job, weapon) - defaultPower) / majorPure;
318 - stats.attackPower.percent -= 1;
319 -
320 - stats.damage.all += 1;
321 - efficiency.damage = (calculatePower(stats, job, weapon) - defaultPower) / majorPure;
322 - stats.damage.all -= 1;
323 -
324 - stats.criticalDamage += 1;
325 - efficiency.criticalDamage = (calculatePower(stats, job, weapon) - defaultPower) / majorPure;
326 - stats.criticalDamage -= 1;
327 -
328 - // 곱연산
329 - const ignoreGuardSaved = stats.ignoreGuard;
330 - stats.ignoreGuard = (1 - (1 - stats.ignoreGuard / 100) * 0.99) * 100;
331 - efficiency.ignoreGuard = (calculatePower(stats, job, weapon) - defaultPower) / majorPure;
332 - stats.ignoreGuard = ignoreGuardSaved;
333 -
334 - return efficiency;
335 -}
336 -
337 -// 버프 적용 스탯 구하기
338 -const getBuffStats = function(stats, job) {
339 - const jobModel = require('../model/job');
340 - const buff = jobModel[job].stats.active;
341 - const defaultBuff = jobModel.default.stats.active;
342 -
343 - return {
344 - major: {
345 - pure: stats.major.pure + buff.major.pure,
346 - percent: stats.major.percent + buff.major.percent,
347 - added: stats.major.added
348 - },
349 - minor: stats.minor,
350 - damage: {
351 - all: stats.damage.all + buff.damage.all + defaultBuff.damage.all,
352 - boss: stats.damage.boss + buff.damage.boss + defaultBuff.damage.boss
353 - },
354 - finalDamage: stats.finalDamage,
355 - criticalDamage: stats.criticalDamage + buff.criticalDamage + defaultBuff.criticalDamage,
356 - attackPower: {
357 - pure: stats.attackPower.pure + buff.attackPower.pure,
358 - percent: stats.attackPower.percent + buff.attackPower.percent + defaultBuff.attackPower.percent
359 - },
360 - ignoreGuard: (1 - (1 - (stats.ignoreGuard / 100)) * (1 - (buff.ignoreGuard / 100)) * (1 - (defaultBuff.ignoreGuard / 100))) * 100
361 - };
362 -}
363 -
364 -// 크리티컬 데미지, 보스 공격력, 방어율 무시를 반영하여 방어율 300% 몬스터 공격시 데미지 산출 값
365 -const calculatePower = function(stats, job, weapon) {
366 - const jobConst = require('../model/job')[job].jobConst;
367 - const weaponConst = require('../model/weapon')[weapon];
368 - return Math.max(
369 - (
370 - (stats.major.pure * (1 + stats.major.percent / 100) + stats.major.added) * 4 +
371 - stats.minor
372 - ) *
373 - 0.01 *
374 - (stats.attackPower.pure * (1 + stats.attackPower.percent / 100)) *
375 - jobConst *
376 - weaponConst *
377 - (1 + stats.damage.all / 100 + stats.damage.boss / 100) *
378 - (1 + stats.finalDamage / 100) *
379 - (1.35 + stats.criticalDamage / 100) *
380 - (1 - 3 * (1 - stats.ignoreGuard / 100)),
381 - 1);
382 -}
383 3
384 module.exports = { 4 module.exports = {
385 getCharacter: async function(req, res) { 5 getCharacter: async function(req, res) {
...@@ -389,7 +9,7 @@ module.exports = { ...@@ -389,7 +9,7 @@ module.exports = {
389 } 9 }
390 10
391 const nickname = req.query.nickname; 11 const nickname = req.query.nickname;
392 - const characterCode = await crwalCharacterCode(req.query.nickname); 12 + const characterCode = await characterModel.crwalCharacterCode(req.query.nickname);
393 13
394 if (characterCode == -1) { 14 if (characterCode == -1) {
395 res.status(500).send(); 15 res.status(500).send();
...@@ -399,7 +19,7 @@ module.exports = { ...@@ -399,7 +19,7 @@ module.exports = {
399 return; 19 return;
400 } 20 }
401 21
402 - const characterInfo = await getCharacterInfo(nickname, characterCode); 22 + const characterInfo = await characterModel.getCharacterInfo(nickname, characterCode);
403 if (characterInfo == -1) { 23 if (characterInfo == -1) {
404 // 접근 권한 설정 필요 24 // 접근 권한 설정 필요
405 res.status(403).send(); 25 res.status(403).send();
...@@ -413,7 +33,7 @@ module.exports = { ...@@ -413,7 +33,7 @@ module.exports = {
413 return; 33 return;
414 } 34 }
415 35
416 - const analysisEquipment = await analyzeEquipment(nickname, characterCode, characterInfo.character.job); 36 + const analysisEquipment = await characterModel.analyzeEquipment(nickname, characterCode, characterInfo.character.job);
417 if (analysisEquipment == -1) { 37 if (analysisEquipment == -1) {
418 // 접근 권한 설정 필요 38 // 접근 권한 설정 필요
419 res.status(403).send(); 39 res.status(403).send();
...@@ -427,21 +47,19 @@ module.exports = { ...@@ -427,21 +47,19 @@ module.exports = {
427 return; 47 return;
428 } 48 }
429 49
430 - const stats = analyzeStats(characterInfo, analysisEquipment); 50 + const stats = analysisModel.analyzeStats(characterInfo, analysisEquipment);
431 - const buffStats = getBuffStats(stats, characterInfo.character.job); 51 + const buffStats = analysisModel.getBuffStats(stats, characterInfo.character.job);
432 - const efficiency = calculateEfficiency(stats, characterInfo.character.job, analysisEquipment.weapon);
433 - const buffEfficiency = calculateEfficiency(buffStats, characterInfo.character.job, analysisEquipment.weapon);
434 52
435 const result = { 53 const result = {
436 info: characterInfo.character, 54 info: characterInfo.character,
437 analysis: { 55 analysis: {
438 default: { 56 default: {
439 stats: stats, 57 stats: stats,
440 - efficiency: efficiency 58 + efficiency: analysisModel.calculateEfficiency(stats, characterInfo.character.job, analysisEquipment.weapon)
441 }, 59 },
442 buff: { 60 buff: {
443 stats: buffStats, 61 stats: buffStats,
444 - efficiency: buffEfficiency 62 + efficiency: analysisModel.calculateEfficiency(buffStats, characterInfo.character.job, analysisEquipment.weapon)
445 } 63 }
446 } 64 }
447 }; 65 };
......
1 -*Looking for a shareable component template? Go here --> [sveltejs/component-template](https://github.com/sveltejs/component-template)*
2 -
3 ----
4 -
5 -# svelte app
6 -
7 -This is a project template for [Svelte](https://svelte.dev) apps. It lives at https://github.com/sveltejs/template.
8 -
9 -To create a new project based on this template using [degit](https://github.com/Rich-Harris/degit):
10 -
11 -```bash
12 -npx degit sveltejs/template svelte-app
13 -cd svelte-app
14 -```
15 -
16 -*Note that you will need to have [Node.js](https://nodejs.org) installed.*
17 -
18 -
19 -## Get started
20 -
21 -Install the dependencies...
22 -
23 -```bash
24 -cd svelte-app
25 -npm install
26 -```
27 -
28 -...then start [Rollup](https://rollupjs.org):
29 -
30 -```bash
31 -npm run dev
32 -```
33 -
34 -Navigate to [localhost:5000](http://localhost:5000). You should see your app running. Edit a component file in `src`, save it, and reload the page to see your changes.
35 -
36 -By default, the server will only respond to requests from localhost. To allow connections from other computers, edit the `sirv` commands in package.json to include the option `--host 0.0.0.0`.
37 -
38 -
39 -## Building and running in production mode
40 -
41 -To create an optimised version of the app:
42 -
43 -```bash
44 -npm run build
45 -```
46 -
47 -You can run the newly built app with `npm run start`. This uses [sirv](https://github.com/lukeed/sirv), which is included in your package.json's `dependencies` so that the app will work when you deploy to platforms like [Heroku](https://heroku.com).
48 -
49 -
50 -## Single-page app mode
51 -
52 -By default, sirv will only respond to requests that match files in `public`. This is to maximise compatibility with static fileservers, allowing you to deploy your app anywhere.
53 -
54 -If you're building a single-page app (SPA) with multiple routes, sirv needs to be able to respond to requests for *any* path. You can make it so by editing the `"start"` command in package.json:
55 -
56 -```js
57 -"start": "sirv public --single"
58 -```
59 -
60 -
61 -## Deploying to the web
62 -
63 -### With [now](https://zeit.co/now)
64 -
65 -Install `now` if you haven't already:
66 -
67 -```bash
68 -npm install -g now
69 -```
70 -
71 -Then, from within your project folder:
72 -
73 -```bash
74 -cd public
75 -now deploy --name my-project
76 -```
77 -
78 -As an alternative, use the [Now desktop client](https://zeit.co/download) and simply drag the unzipped project folder to the taskbar icon.
79 -
80 -### With [surge](https://surge.sh/)
81 -
82 -Install `surge` if you haven't already:
83 -
84 -```bash
85 -npm install -g surge
86 -```
87 -
88 -Then, from within your project folder:
89 -
90 -```bash
91 -npm run build
92 -surge public my-project.surge.sh
93 -```