a collection of tools for fly for fun universe
skillulator.lol
1import { produce } from "immer";
2import { create } from "zustand";
3import { immer } from "zustand/middleware/immer";
4import { tree as jobTree } from "@/data/tree";
5import {
6 getJobById,
7 getJobTotalSkillPoints,
8 getSkillById,
9 updateSkillRequirements,
10} from "@/utils/skill-tree-helpers";
11import { JOB_SKILLPOINTS } from "@/utils/constants";
12
13export type State = {
14 jobTree: typeof jobTree;
15 skillPoints: number;
16 JOB_SKILLPOINTS: typeof JOB_SKILLPOINTS;
17};
18
19type Actions = {
20 increaseSkillPoint: ({
21 jobId,
22 skillId,
23 masterVariationSkillId,
24 }: {
25 jobId: number;
26 skillId: number;
27 masterVariationSkillId?: number;
28 }) => void;
29 increaseSkillToMax: ({
30 jobId,
31 skillId,
32 masterVariationSkillId,
33 }: {
34 jobId: number;
35 skillId: number;
36 masterVariationSkillId?: number;
37 }) => void;
38 decreaseSkillPoint: ({
39 jobId,
40 skillId,
41 masterVariationSkillId,
42 }: {
43 jobId: number;
44 skillId: number;
45 masterVariationSkillId?: number;
46 }) => void;
47 initSkillPoints: (jobId: number, characterLevel: number) => void;
48
49 createPreloadedSkillTree: (
50 jobId: number,
51 skills: Array<Record<string, number>>,
52 ) => void;
53 resetSkillTree: (jobId: number) => void;
54};
55
56const initialState: State = {
57 jobTree,
58 JOB_SKILLPOINTS,
59 skillPoints: 0,
60};
61
62export const useTreeStore = create<State & Actions>()(
63 immer((set) => ({
64 jobTree,
65 JOB_SKILLPOINTS,
66 skillPoints: 0,
67 initSkillPoints: (jobId: number, characterLevel: number) =>
68 set(
69 produce((state: State) => {
70 // need to figure out how many skill points are already spent and subtract it
71 const totalSkillPoints = getJobTotalSkillPoints(
72 state.JOB_SKILLPOINTS,
73 jobId,
74 characterLevel,
75 );
76
77 const job = getJobById(jobId, state.jobTree);
78 if (!job) return state;
79
80 // i didn't write this but it works so w/e
81 const spentSkillPoints = job.skills.reduce((total, skill) => {
82 const base = (skill.skillLevel || 0) * (skill.points || 0);
83 const mv =
84 skill.masterVariations?.reduce(
85 (sum, mvSkill) =>
86 sum + (mvSkill.skillLevel || 0) * (mvSkill.points || 0),
87 0,
88 ) || 0;
89 return total + base + mv;
90 }, 0);
91
92 const remainingSkillPoints = totalSkillPoints - spentSkillPoints;
93
94 state.skillPoints = remainingSkillPoints;
95 return state;
96 }),
97 ),
98 increaseSkillPoint: ({
99 jobId,
100 skillId,
101 masterVariationSkillId,
102 }: {
103 jobId: number;
104 skillId: number;
105 masterVariationSkillId?: number;
106 }) =>
107 set(
108 produce((state: State) => {
109 const job = getJobById(jobId, state.jobTree);
110 if (!job) return state;
111
112 /**
113 * Master variation skills don't live in the outer skill array, only within the skill they are variations of...
114 */
115 const skill = getSkillById(skillId, job.skills);
116
117 if (
118 skill?.masterVariations?.length &&
119 typeof masterVariationSkillId !== "undefined"
120 ) {
121 if (!masterVariationSkillId) return state;
122 const masterVariationSkill = getSkillById(
123 masterVariationSkillId,
124 skill.masterVariations,
125 );
126
127 if (!masterVariationSkill) return state;
128
129 if (masterVariationSkill.skillLevel === masterVariationSkill.levels)
130 return state;
131 if (masterVariationSkill.points > state.skillPoints) return state;
132
133 masterVariationSkill.skillLevel += 1;
134 state.skillPoints -= masterVariationSkill.points;
135
136 updateSkillRequirements(
137 job,
138 masterVariationSkillId,
139 masterVariationSkill.skillLevel,
140 );
141 return state;
142 }
143
144 if (!skill) return state;
145
146 if (skill.skillLevel === skill.levels) return state;
147 if (skill.points > state.skillPoints) return state;
148
149 skill.skillLevel += 1;
150 state.skillPoints -= skill.points;
151
152 updateSkillRequirements(job, skillId, skill.skillLevel);
153 return state;
154 }),
155 ),
156 decreaseSkillPoint: ({
157 jobId,
158 skillId,
159 masterVariationSkillId,
160 }: {
161 jobId: number;
162 skillId: number;
163 masterVariationSkillId?: number;
164 }) =>
165 set(
166 produce((state: State) => {
167 const job = getJobById(jobId, state.jobTree);
168 if (!job) return state;
169
170 const skill = getSkillById(skillId, job.skills);
171 if (
172 skill?.masterVariations?.length &&
173 typeof masterVariationSkillId !== "undefined"
174 ) {
175 if (!masterVariationSkillId) return state;
176 const masterVariationSkill = getSkillById(
177 masterVariationSkillId,
178 skill.masterVariations,
179 );
180
181 if (!masterVariationSkill) return state;
182 if (masterVariationSkill.skillLevel === 0) {
183 return state;
184 }
185
186 masterVariationSkill.skillLevel -= 1;
187 state.skillPoints += masterVariationSkill.points;
188
189 updateSkillRequirements(
190 job,
191 masterVariationSkillId,
192 masterVariationSkill.skillLevel,
193 );
194 return state;
195 }
196
197 if (!skill || skill.skillLevel === 0) return state;
198
199 skill.skillLevel -= 1;
200 state.skillPoints += skill.points;
201
202 updateSkillRequirements(job, skillId, skill.skillLevel);
203 return state;
204 }),
205 ),
206 createPreloadedSkillTree: (
207 jobId: number,
208 predefinedSkills: Array<Record<string, number>>,
209 ) =>
210 set(
211 produce((state: State) => {
212 // we map over the pre-defined skills
213 // we check the skill id and level and update the skillLevel accordingly
214 // we also need to do the check to see if the skill is the min level
215 const job = getJobById(jobId, state.jobTree);
216 if (!job) return state;
217
218 for (const originalTreeSkill of job.skills) {
219 // set level for the base skill if present in predefinedSkills
220 for (const predefinedTreeSkill of predefinedSkills) {
221 if (originalTreeSkill.id === predefinedTreeSkill.skill) {
222 originalTreeSkill.skillLevel = predefinedTreeSkill.level;
223 }
224 }
225
226 // also handle any master variation skills that live inside this skill
227 if (originalTreeSkill.masterVariations?.length) {
228 for (const mvSkill of originalTreeSkill.masterVariations) {
229 for (const predefinedTreeSkill of predefinedSkills) {
230 if (mvSkill.id === predefinedTreeSkill.skill) {
231 mvSkill.skillLevel = predefinedTreeSkill.level;
232 }
233 }
234 // update requirements for this master variation skill
235 updateSkillRequirements(job, mvSkill.id, mvSkill.skillLevel);
236 }
237 }
238
239 const skill = getSkillById(originalTreeSkill.id, job.skills);
240 if (skill) {
241 updateSkillRequirements(
242 job,
243 originalTreeSkill.id,
244 skill.skillLevel,
245 );
246 }
247 }
248 return state;
249 }),
250 ),
251 increaseSkillToMax: ({
252 jobId,
253 skillId,
254 masterVariationSkillId,
255 }: {
256 jobId: number;
257 skillId: number;
258 masterVariationSkillId?: number;
259 }) =>
260 set(
261 produce((state: State) => {
262 const job = getJobById(jobId, state.jobTree);
263 if (!job) return state;
264
265 const skill = getSkillById(skillId, job.skills);
266 if (
267 skill?.masterVariations?.length &&
268 typeof masterVariationSkillId !== "undefined"
269 ) {
270 if (!masterVariationSkillId) return state;
271 const masterVariationSkill = getSkillById(
272 masterVariationSkillId,
273 skill.masterVariations,
274 );
275
276 if (!masterVariationSkill) return state;
277 if (masterVariationSkill.levels === masterVariationSkill.skillLevel)
278 return state;
279
280 const maxPossibleLevel = Math.min(
281 masterVariationSkill.levels,
282 Math.floor(state.skillPoints / masterVariationSkill.points),
283 );
284
285 if (maxPossibleLevel > masterVariationSkill.skillLevel) {
286 const levelsToAdd =
287 maxPossibleLevel - masterVariationSkill.skillLevel;
288 masterVariationSkill.skillLevel = maxPossibleLevel;
289 state.skillPoints -= levelsToAdd * masterVariationSkill.points;
290 }
291
292 updateSkillRequirements(
293 job,
294 masterVariationSkillId,
295 masterVariationSkill.skillLevel,
296 );
297 return state;
298 }
299 if (!skill) return state;
300
301 if (skill.levels === skill.skillLevel) return state;
302
303 const maxPossibleLevel = Math.min(
304 skill.levels,
305 Math.floor(state.skillPoints / skill.points),
306 );
307
308 if (maxPossibleLevel > skill.skillLevel) {
309 const levelsToAdd = maxPossibleLevel - skill.skillLevel;
310 skill.skillLevel = maxPossibleLevel;
311 state.skillPoints -= levelsToAdd * skill.points;
312 }
313
314 updateSkillRequirements(job, skillId, skill.skillLevel);
315 return state;
316 }),
317 ),
318 resetSkillTree: (jobId: number) =>
319 set((state: State) => {
320 const skillPoints = getJobTotalSkillPoints(
321 state.JOB_SKILLPOINTS,
322 jobId,
323 15,
324 );
325
326 return {
327 ...initialState,
328 skillPoints,
329 };
330 }),
331 })),
332);