+28
-22
apps/skillulator/src/index.css
+28
-22
apps/skillulator/src/index.css
···
9
9
@import "tailwindcss";
10
10
11
11
@layer base {
12
-
html,
13
-
body {
14
-
@apply bg-gray-50;
15
-
height: 100%;
16
-
}
17
12
18
-
.a11y-focus {
19
-
@apply focus:border-transparent focus:outline-transparent focus:ring-4 focus:ring-offset-2 focus:ring-indigo-500;
20
-
}
13
+
html,
14
+
body {
15
+
@apply bg-gray-50;
16
+
height: 100%;
17
+
}
21
18
22
-
.skill-level {
23
-
position: absolute;
24
-
bottom: 1px;
25
-
right: 5px;
26
-
font-size: 16px;
27
-
font-weight: bold;
28
-
color: white;
29
-
text-shadow: -1px -1px 0 #f00, 1px -1px 0 #f00, -1px 1px 0 #f00, 1px 1px 0
30
-
#f00;
31
-
}
19
+
.a11y-focus {
20
+
@apply focus:border-transparent focus:outline-transparent focus:ring-4 focus:ring-offset-2 focus:ring-indigo-500;
21
+
}
32
22
33
-
#root {
34
-
height: 100%;
35
-
@apply flex flex-col;
36
-
}
23
+
.skill-level {
24
+
position: absolute;
25
+
bottom: 1px;
26
+
right: 5px;
27
+
font-size: 16px;
28
+
font-weight: bold;
29
+
color: white;
30
+
text-shadow: -1px -1px 0 #f00, 1px -1px 0 #f00, -1px 1px 0 #f00, 1px 1px 0 #f00;
31
+
}
32
+
33
+
#attributesPopover {
34
+
position-anchor: --attributes;
35
+
top: anchor(bottom);
36
+
right: anchor(right);
37
+
}
38
+
39
+
#root {
40
+
height: 100%;
41
+
@apply flex flex-col;
42
+
}
37
43
}
+298
apps/skillulator/src/routes/c/$class/components/Tooltip.tsx
+298
apps/skillulator/src/routes/c/$class/components/Tooltip.tsx
···
1
+
import type { Language, Skill } from "@/types";
2
+
import type { RefObject } from "react";
3
+
4
+
type TooltipProps = {
5
+
skill: Skill;
6
+
popoverRef: RefObject<HTMLDivElement | null>;
7
+
locale: Language;
8
+
};
9
+
10
+
// https://github.com/Frostiae/Flyffulator/blob/main/src/flyff/flyfftooltip.jsx#L535
11
+
function renderSkillAttributes(skill: Skill, locale: Language) {
12
+
const out = [];
13
+
const skillLevel = skill.skillLevel ?? skill.levels;
14
+
const skillAttribute =
15
+
skill.skillLevel !== undefined
16
+
? skill.skillStats[skillLevel]
17
+
: skill.skillStats[0];
18
+
19
+
if (skill.element !== "none") {
20
+
out.push(`\nElement: ${skill.element}`);
21
+
}
22
+
23
+
if (skillAttribute.consumedMP !== undefined) {
24
+
out.push(`\nMP: ${skillAttribute.consumedMP}`);
25
+
}
26
+
27
+
if (skillAttribute.consumedFP !== undefined) {
28
+
out.push(`\nFP: ${skillAttribute.consumedFP}`);
29
+
}
30
+
31
+
// Attack
32
+
if (skillAttribute.maxAttack !== undefined && skillAttribute.maxAttack > 0) {
33
+
out.push(
34
+
<span>
35
+
Base Damage: {skillAttribute.minAttack} ~ {skillAttribute.maxAttack}
36
+
</span>,
37
+
);
38
+
}
39
+
40
+
if (skillAttribute.scalingParameters !== undefined) {
41
+
for (const scale of skillAttribute.scalingParameters) {
42
+
if (scale.parameter === "attack" && scale.maximum === undefined) {
43
+
out.push(
44
+
<span>
45
+
<br />
46
+
Attack Scaling: {scale.stat} x {scale.scale}
47
+
</span>,
48
+
);
49
+
}
50
+
}
51
+
}
52
+
53
+
// Heal
54
+
if (skillAttribute.abilities !== undefined) {
55
+
for (const ability of skillAttribute.abilities) {
56
+
if (ability.parameter === "hp") {
57
+
out.push(
58
+
<span>
59
+
<br />
60
+
Base Heal: {ability.add}
61
+
</span>,
62
+
);
63
+
break;
64
+
}
65
+
}
66
+
}
67
+
68
+
if (skillAttribute.scalingParameters !== undefined) {
69
+
for (const scale of skillAttribute.scalingParameters) {
70
+
if (scale.parameter === "hp") {
71
+
out.push(
72
+
<span>
73
+
<br />
74
+
Heal Scaling: {scale.stat} x {scale.scale}
75
+
</span>,
76
+
);
77
+
}
78
+
}
79
+
}
80
+
81
+
// Time
82
+
if (skillAttribute.duration !== undefined) {
83
+
const secs = skillAttribute.duration % 60;
84
+
const mins = Math.floor(skillAttribute.duration / 60);
85
+
out.push(
86
+
<span>
87
+
<br />
88
+
Base Time: {String(mins).padStart(2, "0")}:
89
+
{String(secs).padStart(2, "0")}
90
+
</span>,
91
+
);
92
+
}
93
+
94
+
if (skillAttribute.scalingParameters !== undefined) {
95
+
for (const scale of skillAttribute.scalingParameters) {
96
+
if (scale.parameter === "duration") {
97
+
out.push(
98
+
<span>
99
+
<br />
100
+
Time Scaling: {scale.stat} x {scale.scale}
101
+
</span>,
102
+
);
103
+
}
104
+
}
105
+
}
106
+
107
+
// Casting time
108
+
if (skillAttribute.casting !== undefined && skillAttribute.casting >= 1) {
109
+
const secs = skillAttribute.casting % 60;
110
+
const mins = Math.floor(skillAttribute.casting / 60);
111
+
out.push(
112
+
<span>
113
+
<br />
114
+
Casting Time: {String(mins).padStart(2, "0")}:
115
+
{String(secs).padStart(2, "0")}
116
+
</span>,
117
+
);
118
+
}
119
+
120
+
// Cooldown
121
+
// TODO: PvP cooldown seems to be missing from the API, check Holycross for example
122
+
if (skillAttribute.cooldown !== undefined) {
123
+
const secs = Math.ceil(skillAttribute.cooldown) % 60;
124
+
const mins = Math.floor(Math.ceil(skillAttribute.cooldown) / 60);
125
+
out.push(
126
+
<span>
127
+
<br />
128
+
Cooldown: {String(mins).padStart(2, "0")}:
129
+
{String(secs).padStart(2, "0")}
130
+
</span>,
131
+
);
132
+
}
133
+
134
+
// Range
135
+
if (skillAttribute.spellRange !== undefined) {
136
+
out.push(
137
+
<span>
138
+
<br />
139
+
Spell Range: {skillAttribute.spellRange}
140
+
</span>,
141
+
);
142
+
143
+
if (skill.target === "party") {
144
+
out.push(<span> (Party)</span>);
145
+
} else if (skill.target === "area") {
146
+
out.push(<span> (Around)</span>);
147
+
}
148
+
}
149
+
150
+
// Probability
151
+
if (skillAttribute.probability !== undefined) {
152
+
out.push(
153
+
<span>
154
+
<br />
155
+
Probability: {skillAttribute.probability}%
156
+
</span>,
157
+
);
158
+
159
+
if (
160
+
skillAttribute.probabilityPVP !== undefined &&
161
+
skillAttribute.probabilityPVP !== skillAttribute.probability
162
+
) {
163
+
out.push(<span> / {skillAttribute.probabilityPVP}% (PVP & Giants)</span>);
164
+
}
165
+
}
166
+
167
+
// TODO: wallLives missing from elementor skill
168
+
if (skillAttribute.wallLives !== undefined) {
169
+
out.push(
170
+
<span>
171
+
<br />
172
+
Number of Lives: {skillAttribute.wallLives}
173
+
</span>,
174
+
);
175
+
}
176
+
177
+
// Reflex hit
178
+
if (
179
+
skillAttribute.reflectedDamagePVE !== undefined &&
180
+
skillAttribute.reflectedDamagePVP !== undefined
181
+
) {
182
+
out.push(
183
+
<span>
184
+
<br />
185
+
Reflected Damage: {skillAttribute.reflectedDamagePVE}% /{" "}
186
+
{skillAttribute.reflectedDamagePVP}% (PVP)
187
+
</span>,
188
+
);
189
+
}
190
+
191
+
// Damage over time
192
+
if (skillAttribute.dotTick !== undefined) {
193
+
out.push(
194
+
<span>
195
+
<br />
196
+
DoT Tick: {skillAttribute.dotTick} Seconds
197
+
</span>,
198
+
);
199
+
}
200
+
201
+
// Stats
202
+
// if (skillAttribute.abilities !== undefined) {
203
+
// for (const ability of skillAttribute.abilities) {
204
+
// const abilityStyle = { color: "#6161ff" };
205
+
// const add = ability.add;
206
+
// let extra = 0;
207
+
208
+
// if (skillAttribute.scalingParameters !== undefined) {
209
+
// for (const scale of skillAttribute.scalingParameters) {
210
+
// if (
211
+
// scale.parameter === ability.parameter &&
212
+
// scale.maximum !== undefined
213
+
// ) {
214
+
// let bufferStat = 0;
215
+
// switch (scale.stat) {
216
+
// case "int":
217
+
// bufferStat = Context.player.bufferInt;
218
+
// break;
219
+
// case "str":
220
+
// bufferStat = Context.player.bufferStr;
221
+
// break;
222
+
// case "dex":
223
+
// bufferStat = Context.player.bufferDex;
224
+
// break;
225
+
// default:
226
+
// bufferStat = Context.player.bufferSta;
227
+
// break;
228
+
// }
229
+
230
+
// extra = Math.floor(
231
+
// Math.min(scale.scale * bufferStat, scale.maximum),
232
+
// );
233
+
// }
234
+
// }
235
+
// }
236
+
237
+
// out.push(
238
+
// <span style={abilityStyle}>
239
+
// <br />
240
+
// {Utils.getStatNameByIdOrDefault(ability.parameter, i18n)}
241
+
// {ability.set != undefined ? "=" : "+"}
242
+
// {ability.set != undefined ? ability.set : add + extra}
243
+
// {ability.rate && "%"}
244
+
// </span>,
245
+
// );
246
+
// if (extra > 0) {
247
+
// out.push(
248
+
// <span style={{ color: "#ffaa00" }}>
249
+
// {" "}
250
+
// ({add}+{extra})
251
+
// </span>,
252
+
// );
253
+
// }
254
+
// }
255
+
256
+
// if (skillAttribute.scalingParameters !== undefined) {
257
+
// for (const ability of skillAttribute.abilities) {
258
+
// for (const scale of skillAttribute.scalingParameters) {
259
+
// if (
260
+
// scale.parameter === ability.parameter &&
261
+
// scale.maximum !== undefined
262
+
// ) {
263
+
// out.push(
264
+
// <span style={{ color: "#ffaa00" }}>
265
+
// <br />
266
+
// {scale.parameter} Scaling: +{scale.scale * 25}
267
+
// {ability.rate && "%"} per 25 {scale.stat} (max {scale.maximum}
268
+
// {ability.rate && "%"})
269
+
// </span>,
270
+
// );
271
+
// }
272
+
// }
273
+
// }
274
+
// }
275
+
// }
276
+
277
+
// out.push(`\n${skill.description[locale] ?? skill.description.en}`);
278
+
279
+
return out.map((result) => result);
280
+
}
281
+
282
+
export function Tooltip(props: TooltipProps) {
283
+
return (
284
+
<div
285
+
id="attributesPopover"
286
+
popover="auto"
287
+
ref={props.popoverRef}
288
+
className="absolute m-0 inset-auto h-[300px] w-[300px] shadow-sm border border-gray-100 rounded-sm p-4"
289
+
>
290
+
<div className="flex gap-1">
291
+
<h2 className="font-bold">{props.skill.name[props.locale]}</h2>
292
+
·
293
+
<span>Lv. {props.skill.level}</span>
294
+
</div>
295
+
<div>{renderSkillAttributes(props.skill, props.locale)}</div>
296
+
</div>
297
+
);
298
+
}