-34
apps/skillulator/src/contstants.ts
-34
apps/skillulator/src/contstants.ts
···
1
-
export const JOBS = [
2
-
{
3
-
name: "blade",
4
-
image: "blade.png",
5
-
},
6
-
{
7
-
name: "knight",
8
-
image: "knight.png",
9
-
},
10
-
{
11
-
name: "elementor",
12
-
image: "elementor.png",
13
-
},
14
-
{
15
-
name: "psykeeper",
16
-
image: "psychikeeper.png",
17
-
},
18
-
{
19
-
name: "billposter",
20
-
image: "billposter.png",
21
-
},
22
-
{
23
-
name: "ringmaster",
24
-
image: "ringmaster.png",
25
-
},
26
-
{
27
-
name: "ranger",
28
-
image: "ranger.png",
29
-
},
30
-
{
31
-
name: "jester",
32
-
image: "jester.png",
33
-
},
34
-
];
+17
-17
apps/skillulator/src/i18n.ts
+17
-17
apps/skillulator/src/i18n.ts
···
3
3
4
4
import Backend from "i18next-http-backend";
5
5
import LanguageDetector from "i18next-browser-languagedetector";
6
-
import { languages } from "./utils";
6
+
import { languages } from "./utils/constants";
7
7
8
8
i18n
9
-
.use(Backend)
10
-
.use(LanguageDetector)
11
-
.use(initReactI18next)
12
-
.init({
13
-
fallbackLng: "en",
14
-
lng: "en",
15
-
debug: false,
16
-
interpolation: {
17
-
escapeValue: false,
18
-
},
19
-
});
9
+
.use(Backend)
10
+
.use(LanguageDetector)
11
+
.use(initReactI18next)
12
+
.init({
13
+
fallbackLng: "en",
14
+
lng: "en",
15
+
debug: false,
16
+
interpolation: {
17
+
escapeValue: false,
18
+
},
19
+
});
20
20
21
21
i18n.on("languageChanged", (lng) => {
22
-
const htmlLang = languages.find((lang) => lang.label === lng);
23
-
document.documentElement.setAttribute(
24
-
"lang",
25
-
htmlLang?.locale ? htmlLang.locale : htmlLang!.label
26
-
);
22
+
const htmlLang = languages.find((lang) => lang.label === lng);
23
+
document.documentElement.setAttribute(
24
+
"lang",
25
+
htmlLang?.locale ? htmlLang.locale : "en",
26
+
);
27
27
});
28
28
29
29
export default i18n;
+8
-8
apps/skillulator/src/routes/__root.tsx
+8
-8
apps/skillulator/src/routes/__root.tsx
···
1
-
import { createRootRoute, Link, Outlet } from "@tanstack/react-router";
1
+
import { createRootRoute, Outlet } from "@tanstack/react-router";
2
2
import { Navbar } from "../components/Navbar";
3
3
import { Footer } from "../components/Footer";
4
4
5
5
export const Route = createRootRoute({
6
-
component: () => (
7
-
<>
8
-
<Navbar />
9
-
<Outlet />
10
-
<Footer />
11
-
</>
12
-
),
6
+
component: () => (
7
+
<>
8
+
<Navbar />
9
+
<Outlet />
10
+
<Footer />
11
+
</>
12
+
),
13
13
});
+5
-1
apps/skillulator/src/routes/c.$class.tsx
+5
-1
apps/skillulator/src/routes/c.$class.tsx
···
11
11
import { useTranslation } from "react-i18next";
12
12
import { Link, useNavigate, useParams } from "@tanstack/react-router";
13
13
import Skill from "../components/Skill";
14
-
import { decodeTree, encodeTree, getJobByName } from "../utils/index";
14
+
import {
15
+
decodeTree,
16
+
encodeTree,
17
+
getJobByName,
18
+
} from "../utils/skill-tree-helpers";
15
19
import { useTreeStore } from "../zustand/treeStore";
16
20
import { t } from "i18next";
17
21
+36
-35
apps/skillulator/src/routes/index.lazy.tsx
+36
-35
apps/skillulator/src/routes/index.lazy.tsx
···
1
1
import { Suspense } from "react";
2
2
import { useTranslation } from "react-i18next";
3
-
import { JOBS } from "../contstants";
3
+
import { JOBS } from "../utils/constants";
4
4
import { createLazyFileRoute, Link } from "@tanstack/react-router";
5
5
6
6
export const Route = createLazyFileRoute("/")({
7
-
component: Index,
7
+
component: Index,
8
8
});
9
9
10
10
function Index() {
11
-
const { t } = useTranslation();
11
+
const { t } = useTranslation();
12
12
13
-
return (
14
-
<>
15
-
<Suspense fallback="loading...">
16
-
<main className="flex flex-col items-center justify-center px-3 pt-32 pb-3">
17
-
<h1 className="mb-4 text-3xl font-bold">Skillulator</h1>
18
-
<div className="grid w-full grid-cols-2 gap-2 mb-4 lg:w-max lg:grid-cols-4">
19
-
{JOBS.map((job) => (
20
-
<Link
21
-
aria-label={`Go to the ${job.name} skill tree`}
22
-
to={`/c/${job.name}`}
23
-
key={job.name}
24
-
className="flex flex-col items-center justify-center px-1 py-2 duration-150 bg-white border border-gray-300 rounded-md hover:bg-gray-100 lg:px-5 a11y-focus"
25
-
>
26
-
<img
27
-
src={`https://skillulator.lol/icons/classes/${job.image}`}
28
-
className="w-10 h-10 md:h-12 md:w-12"
29
-
/>
30
-
<span className="capitalize">{job.name}</span>
31
-
</Link>
32
-
))}
33
-
</div>
34
-
<h2 className="text-lg font-bold">{t("secondaryTitle")}</h2>
35
-
<ul className="text-sm list-disc list-inside">
36
-
<li>{t("appInstructions.inst1")}</li>
37
-
<li>{t("appInstructions.inst2")}</li>
38
-
<li>{t("appInstructions.inst3")}</li>
39
-
<li>{t("appInstructions.inst4")}</li>
40
-
</ul>
41
-
</main>
42
-
</Suspense>
43
-
</>
44
-
);
13
+
return (
14
+
<>
15
+
<Suspense fallback="loading...">
16
+
<main className="flex flex-col items-center justify-center px-3 pt-32 pb-3">
17
+
<h1 className="mb-4 text-3xl font-bold">Skillulator</h1>
18
+
<div className="grid w-full grid-cols-2 gap-2 mb-4 lg:w-max lg:grid-cols-4">
19
+
{JOBS.map((job) => (
20
+
<Link
21
+
aria-label={`Go to the ${job.name} skill tree`}
22
+
to={`/c/${job.name}`}
23
+
key={job.name}
24
+
className="flex flex-col items-center justify-center px-1 py-2 duration-150 bg-white border border-gray-300 rounded-md hover:bg-gray-100 lg:px-5 a11y-focus"
25
+
>
26
+
<img
27
+
alt=""
28
+
src={`https://skillulator.lol/icons/classes/${job.image}`}
29
+
className="w-10 h-10 md:h-12 md:w-12"
30
+
/>
31
+
<span className="capitalize">{job.name}</span>
32
+
</Link>
33
+
))}
34
+
</div>
35
+
<h2 className="text-lg font-bold">{t("secondaryTitle")}</h2>
36
+
<ul className="text-sm list-disc list-inside">
37
+
<li>{t("appInstructions.inst1")}</li>
38
+
<li>{t("appInstructions.inst2")}</li>
39
+
<li>{t("appInstructions.inst3")}</li>
40
+
<li>{t("appInstructions.inst4")}</li>
41
+
</ul>
42
+
</main>
43
+
</Suspense>
44
+
</>
45
+
);
45
46
}
+147
apps/skillulator/src/utils/constants.ts
+147
apps/skillulator/src/utils/constants.ts
···
1
+
export const JOB_SKILLPOINTS = {
2
+
// elementor
3
+
9150: {
4
+
firstJobSP: 90,
5
+
secondJobSP: 300,
6
+
},
7
+
//psykeeper
8
+
5709: {
9
+
firstJobSP: 90,
10
+
secondJobSP: 90,
11
+
},
12
+
// blade
13
+
2246: {
14
+
firstJobSP: 60,
15
+
secondJobSP: 80,
16
+
},
17
+
// knight
18
+
5330: {
19
+
firstJobSP: 60,
20
+
secondJobSP: 80,
21
+
},
22
+
// billposter
23
+
7424: {
24
+
firstJobSP: 60,
25
+
secondJobSP: 120,
26
+
},
27
+
// ringmaster
28
+
9389: {
29
+
firstJobSP: 60,
30
+
secondJobSP: 100,
31
+
},
32
+
// ranger
33
+
9295: {
34
+
firstJobSP: 50,
35
+
secondJobSP: 100,
36
+
},
37
+
// jester
38
+
3545: {
39
+
firstJobSP: 50,
40
+
secondJobSP: 100,
41
+
},
42
+
};
43
+
44
+
export const languages = [
45
+
{
46
+
label: "en",
47
+
value: "en",
48
+
language: "English",
49
+
},
50
+
{
51
+
label: "pt-BR",
52
+
value: "br",
53
+
locale: "pt-BR",
54
+
language: "Português",
55
+
},
56
+
{
57
+
label: "zh",
58
+
value: "cns",
59
+
locale: "zh-CN",
60
+
language: "Chinese",
61
+
},
62
+
{
63
+
label: "ja",
64
+
value: "jp",
65
+
language: "Japanese",
66
+
},
67
+
{
68
+
label: "ko",
69
+
value: "kr",
70
+
language: "Korean",
71
+
},
72
+
{
73
+
label: "es",
74
+
value: "sp",
75
+
language: "Spanish",
76
+
},
77
+
{
78
+
label: "ru",
79
+
value: "ru",
80
+
language: "Russian",
81
+
},
82
+
{
83
+
label: "de",
84
+
value: "de",
85
+
language: "German",
86
+
},
87
+
{
88
+
label: "fi",
89
+
value: "fi",
90
+
language: "Finnish",
91
+
},
92
+
{
93
+
label: "id",
94
+
value: "id",
95
+
language: "Indonesian",
96
+
},
97
+
{
98
+
label: "it",
99
+
value: "it",
100
+
language: "Italian",
101
+
},
102
+
{
103
+
label: "nl",
104
+
value: "nl",
105
+
language: "Dutch",
106
+
},
107
+
{
108
+
label: "pl",
109
+
value: "pl",
110
+
language: "Polish",
111
+
},
112
+
];
113
+
114
+
export const JOBS = [
115
+
{
116
+
name: "blade",
117
+
image: "blade.png",
118
+
},
119
+
{
120
+
name: "knight",
121
+
image: "knight.png",
122
+
},
123
+
{
124
+
name: "elementor",
125
+
image: "elementor.png",
126
+
},
127
+
{
128
+
name: "psykeeper",
129
+
image: "psychikeeper.png",
130
+
},
131
+
{
132
+
name: "billposter",
133
+
image: "billposter.png",
134
+
},
135
+
{
136
+
name: "ringmaster",
137
+
image: "ringmaster.png",
138
+
},
139
+
{
140
+
name: "ranger",
141
+
image: "ranger.png",
142
+
},
143
+
{
144
+
name: "jester",
145
+
image: "jester.png",
146
+
},
147
+
];
-244
apps/skillulator/src/utils/index.ts
-244
apps/skillulator/src/utils/index.ts
···
1
-
import type { State } from "../zustand/treeStore";
2
-
3
-
export function getJobById(jobId: number, jobs: State["jobTree"]) {
4
-
return jobs.filter((job) => job.id === jobId).at(0);
5
-
}
6
-
7
-
export function getSkillById(
8
-
skillId: number,
9
-
skills: State["jobTree"][number]["skills"],
10
-
) {
11
-
return skills.find((skill) => skill.id === skillId);
12
-
}
13
-
14
-
export function getJobByName(jobName: string, jobs: State["jobTree"]) {
15
-
return jobs.find(
16
-
(job) => job.name.en.toLowerCase() === jobName.toLowerCase(),
17
-
);
18
-
}
19
-
20
-
export function encodeTree(
21
-
skills: State["jobTree"][number]["skills"],
22
-
characterLevel: number,
23
-
) {
24
-
return (
25
-
skills?.map((skill) => `${skill.id}:${skill.skillLevel}`).join(",") +
26
-
`#${characterLevel}`
27
-
);
28
-
}
29
-
30
-
export function decodeTree(encodedSkills: string) {
31
-
const characterLevel = encodedSkills.split("#").at(1);
32
-
const decodedTree = encodedSkills.split("#").at(0);
33
-
return {
34
-
untangledSkillMap: decodedTree!
35
-
.split(",")
36
-
.map((skill) => skill.split(":"))
37
-
.map((s) => ({ skill: +s[0], level: +s[1] })),
38
-
characterLevel,
39
-
};
40
-
}
41
-
42
-
export function getSkillPointsForLevel(characterLevel: number) {
43
-
switch (true) {
44
-
case characterLevel >= 15 && characterLevel <= 20:
45
-
return characterLevel * 2;
46
-
case characterLevel > 20 && characterLevel <= 40:
47
-
return (characterLevel - 20) * 3 + 20 * 2;
48
-
case characterLevel > 40 && characterLevel <= 60:
49
-
return (characterLevel - 40) * 4 + 20 * 3 + 20 * 2;
50
-
case characterLevel > 60 && characterLevel <= 80:
51
-
return (characterLevel - 60) * 5 + 20 * 4 + 20 * 3 + 20 * 2;
52
-
case characterLevel > 80 && characterLevel <= 100:
53
-
return (characterLevel - 80) * 6 + 20 * 5 + 20 * 4 + 20 * 3 + 20 * 2;
54
-
case characterLevel > 100 && characterLevel <= 120:
55
-
return (
56
-
(characterLevel - 100) * 7 + 20 * 6 + 20 * 5 + 20 * 4 + 20 * 3 + 20 * 2
57
-
);
58
-
case characterLevel > 120 && characterLevel <= 140:
59
-
return (
60
-
(characterLevel - 120) * 8 +
61
-
20 * 7 +
62
-
20 * 6 +
63
-
20 * 5 +
64
-
20 * 4 +
65
-
20 * 3 +
66
-
20 * 2
67
-
);
68
-
case characterLevel > 140 && characterLevel <= 150:
69
-
return (
70
-
(characterLevel - 140) * 1 +
71
-
20 * 8 +
72
-
20 * 7 +
73
-
20 * 6 +
74
-
20 * 5 +
75
-
20 * 4 +
76
-
20 * 3 +
77
-
20 * 2
78
-
);
79
-
case characterLevel > 150 && characterLevel <= 160:
80
-
return (
81
-
(characterLevel - 150) * 2 +
82
-
20 * 1 +
83
-
20 * 8 +
84
-
20 * 7 +
85
-
20 * 6 +
86
-
20 * 5 +
87
-
20 * 4 +
88
-
20 * 3 +
89
-
20 * 2
90
-
);
91
-
case characterLevel > 160 && characterLevel <= 165:
92
-
return (
93
-
(characterLevel - 160) * 2 +
94
-
20 * 1 +
95
-
20 * 8 +
96
-
20 * 7 +
97
-
20 * 6 +
98
-
20 * 5 +
99
-
20 * 4 +
100
-
20 * 3 +
101
-
20 * 2 +
102
-
20 * 2
103
-
);
104
-
default:
105
-
return 0;
106
-
}
107
-
}
108
-
109
-
// eh this could be named better lol
110
-
export const classSkillPoints = {
111
-
// elementor
112
-
9150: {
113
-
firstJobSP: 90,
114
-
secondJobSP: 300,
115
-
},
116
-
//psykeeper
117
-
5709: {
118
-
firstJobSP: 90,
119
-
secondJobSP: 90,
120
-
},
121
-
// blade
122
-
2246: {
123
-
firstJobSP: 60,
124
-
secondJobSP: 80,
125
-
},
126
-
// knight
127
-
5330: {
128
-
firstJobSP: 60,
129
-
secondJobSP: 80,
130
-
},
131
-
// billposter
132
-
7424: {
133
-
firstJobSP: 60,
134
-
secondJobSP: 120,
135
-
},
136
-
// ringmaster
137
-
9389: {
138
-
firstJobSP: 60,
139
-
secondJobSP: 100,
140
-
},
141
-
// ranger
142
-
9295: {
143
-
firstJobSP: 50,
144
-
secondJobSP: 100,
145
-
},
146
-
// jester
147
-
3545: {
148
-
firstJobSP: 50,
149
-
secondJobSP: 100,
150
-
},
151
-
};
152
-
153
-
export function getJobTotalSkillPoints(
154
-
jobMap: typeof classSkillPoints,
155
-
jobId: number,
156
-
characterLevel: number,
157
-
) {
158
-
if (characterLevel >= 60) {
159
-
return (
160
-
getSkillPointsForLevel(characterLevel) +
161
-
jobMap[jobId].firstJobSP +
162
-
jobMap[jobId].secondJobSP
163
-
);
164
-
}
165
-
166
-
return getSkillPointsForLevel(characterLevel) + jobMap[jobId].firstJobSP;
167
-
}
168
-
169
-
export const languages = [
170
-
{
171
-
label: "en",
172
-
value: "en",
173
-
language: "English",
174
-
},
175
-
{
176
-
label: "pt-BR",
177
-
value: "br",
178
-
locale: "pt-BR",
179
-
language: "Português",
180
-
},
181
-
{
182
-
label: "zh",
183
-
value: "cns",
184
-
locale: "zh-CN",
185
-
language: "Chinese",
186
-
},
187
-
{
188
-
label: "ja",
189
-
value: "jp",
190
-
language: "Japanese",
191
-
},
192
-
{
193
-
label: "ko",
194
-
value: "kr",
195
-
language: "Korean",
196
-
},
197
-
{
198
-
label: "es",
199
-
value: "sp",
200
-
language: "Spanish",
201
-
},
202
-
{
203
-
label: "ru",
204
-
value: "ru",
205
-
language: "Russian",
206
-
},
207
-
{
208
-
label: "de",
209
-
value: "de",
210
-
language: "German",
211
-
},
212
-
{
213
-
label: "fi",
214
-
value: "fi",
215
-
language: "Finnish",
216
-
},
217
-
{
218
-
label: "id",
219
-
value: "id",
220
-
language: "Indonesian",
221
-
},
222
-
{
223
-
label: "it",
224
-
value: "it",
225
-
language: "Italian",
226
-
},
227
-
{
228
-
label: "nl",
229
-
value: "nl",
230
-
language: "Dutch",
231
-
},
232
-
{
233
-
label: "pl",
234
-
value: "pl",
235
-
language: "Polish",
236
-
},
237
-
];
238
-
239
-
export function getLanguageForSkill(
240
-
langs: typeof languages,
241
-
appLanguage: string,
242
-
) {
243
-
return langs.find((lang) => lang.label === appLanguage)?.value;
244
-
}
+8
apps/skillulator/src/utils/language.ts
+8
apps/skillulator/src/utils/language.ts
+94
apps/skillulator/src/utils/skill-tree-helpers.ts
+94
apps/skillulator/src/utils/skill-tree-helpers.ts
···
1
+
import type { State } from "../zustand/treeStore";
2
+
import type { JOB_SKILLPOINTS } from "./constants";
3
+
4
+
type Skills = State["jobTree"][number]["skills"];
5
+
type Jobs = State["jobTree"];
6
+
type SkillBracket = {
7
+
maxLevel: number;
8
+
pointsPerLevel: number;
9
+
};
10
+
11
+
export function getJobById(jobId: number, jobs: Jobs) {
12
+
return jobs.filter((job) => job.id === jobId).at(0);
13
+
}
14
+
15
+
export function getSkillById(skillId: number, skills: Skills) {
16
+
return skills.filter((skill) => skill.id === skillId).at(0);
17
+
}
18
+
19
+
export function getJobByName(jobName: string, jobs: Jobs) {
20
+
return jobs.find(
21
+
(job) => job.name.en.toLowerCase() === jobName.toLowerCase(),
22
+
);
23
+
}
24
+
25
+
export function encodeTree(skills: Skills, characterLevel: number) {
26
+
return `${skills.map((skill) => `${skill.id}:${skill.skillLevel}`).join(",")}#${characterLevel}`;
27
+
}
28
+
29
+
export function decodeTree(encodedSkills: string) {
30
+
const characterLevel = encodedSkills.split("#").at(1);
31
+
const decodedTree = encodedSkills.split("#").at(0);
32
+
if (characterLevel && decodedTree) {
33
+
return {
34
+
normalizedSkillMap: decodedTree
35
+
.split(",")
36
+
.map((skill) => skill.split(":"))
37
+
.map((s) => ({ skill: Number(s[0]), level: Number(s[1]) })),
38
+
characterLevel,
39
+
};
40
+
}
41
+
throw new Error("Could not decode skill tree");
42
+
}
43
+
44
+
const SKILL_BRACKETS: SkillBracket[] = [
45
+
{ maxLevel: 20, pointsPerLevel: 2 },
46
+
{ maxLevel: 40, pointsPerLevel: 3 },
47
+
{ maxLevel: 60, pointsPerLevel: 4 },
48
+
{ maxLevel: 80, pointsPerLevel: 5 },
49
+
{ maxLevel: 100, pointsPerLevel: 6 },
50
+
{ maxLevel: 120, pointsPerLevel: 7 },
51
+
{ maxLevel: 140, pointsPerLevel: 8 },
52
+
{ maxLevel: 150, pointsPerLevel: 1 },
53
+
{ maxLevel: 160, pointsPerLevel: 2 },
54
+
{ maxLevel: 165, pointsPerLevel: 2 },
55
+
];
56
+
57
+
export function getSkillPointsForLevel(characterLevel: number): number {
58
+
if (characterLevel < 15) {
59
+
return 0;
60
+
}
61
+
62
+
let totalPoints = 0;
63
+
let previousMaxLevel = 0;
64
+
65
+
for (const bracket of SKILL_BRACKETS) {
66
+
const effectiveLevel = Math.min(characterLevel, bracket.maxLevel);
67
+
const levelsInBracket = Math.max(0, effectiveLevel - previousMaxLevel);
68
+
69
+
totalPoints += levelsInBracket * bracket.pointsPerLevel;
70
+
previousMaxLevel = bracket.maxLevel;
71
+
72
+
if (characterLevel <= bracket.maxLevel) {
73
+
break;
74
+
}
75
+
}
76
+
77
+
return totalPoints;
78
+
}
79
+
80
+
export function getJobTotalSkillPoints(
81
+
jobMap: typeof JOB_SKILLPOINTS,
82
+
jobId: number,
83
+
characterLevel: number,
84
+
) {
85
+
if (characterLevel >= 60) {
86
+
return (
87
+
getSkillPointsForLevel(characterLevel) +
88
+
jobMap[jobId].firstJobSP +
89
+
jobMap[jobId].secondJobSP
90
+
);
91
+
}
92
+
93
+
return getSkillPointsForLevel(characterLevel) + jobMap[jobId].firstJobSP;
94
+
}