오윤석

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
This diff is collapsed. Click to expand it.
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 -```