Showing
6 changed files
with
479 additions
and
485 deletions
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 | + | ||
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 | ... | ... |
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) { | ||
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 | }; | ... | ... |
app/web/README.md
deleted
100644 → 0
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 | -``` |
images/screenshot.png
0 → 100644

30.6 KB
-
Please register or login to post a comment