1use crate::prelude::*;
2
3use crate::{
4 abilities::AbilityId, currency::CurrencyId, fighting::FightTemplateId, items::AttributeId,
5};
6
7#[declare]
8pub type EntityTemplateId = Uuid;
9
10#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Tsify, JsonSchema)]
11pub struct EntityAttribute {
12 #[schemars(schema_with = "attribute_link_id_schema")]
13 pub attribute_id: AttributeId,
14 #[schemars(title = "Значение атрибута")]
15 pub value: u64,
16}
17
18#[derive(Debug, serde::Deserialize)]
19pub struct CharacterQuery {
20 pub character_id: String,
21}
22
23#[derive(Clone, Debug, Serialize, Deserialize, Tsify, JsonSchema)]
24pub struct EnemyReward {
25 #[schemars(title = "Id валюты награды", schema_with = "currency_link_id_schema")]
26 pub currency_id: CurrencyId,
27 #[schemars(title = "Минимальная награда")]
28 pub from: i64,
29 #[schemars(title = "Максимальная награда")]
30 pub to: i64,
31 #[schemars(
32 title = "Шанс выпадения",
33 description = "Шанс выпадения валюты от 0 до 100"
34 )]
35 pub drop_chance: f64,
36}
37
38impl PartialEq for EnemyReward {
39 fn eq(&self, other: &Self) -> bool {
40 self.currency_id == other.currency_id && self.from == other.from && self.to == other.to
41 }
42}
43
44impl Eq for EnemyReward {}
45
46#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Tsify, JsonSchema)]
47pub struct EntityTemplate {
48 #[schemars(schema_with = "id_schema")]
49 pub id: EntityTemplateId,
50 #[schemars(title = "Имя врага")]
51 pub name: String,
52 #[schemars(title = "Спайн", schema_with = "aa_entity_spine_schema")]
53 pub spine: String,
54 #[schemars(title = "Визуал", schema_with = "asset_unit_spine_skin")]
55 pub spine_skin_path: String,
56 #[schemars(title = "Размер спайна врага")]
57 pub spine_scale: u64,
58 #[schemars(title = "Время каста")]
59 pub cast_time: u64,
60 #[schemars(title = "Атрибуты врага", description = "Набор атрибутов врага")]
61 pub attributes: Vec<EntityAttribute>,
62 #[schemars(
63 title = "Id способностей",
64 description = "Набор id способностей врага",
65 schema_with = "ability_link_id_array_schema"
66 )]
67 pub ability_ids: Vec<AbilityId>,
68 #[schemars(title = "Ширина врага в клетках")]
69 pub width: i8,
70 #[schemars(title = "Награда за убийство врага")]
71 pub rewards: Vec<EnemyReward>,
72
73 #[schemars(
74 title = "Является ли боссом",
75 description = "Босс-сущность (entity_template_is_boss): влияет на босс-таланты и HP-бар."
76 )]
77 #[serde(default)]
78 pub is_boss: bool,
79}
80
81#[derive(
82 Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Tsify, JsonSchema, i18n::Translate,
83)]
84pub struct Chapter {
85 #[schemars(schema_with = "id_schema")]
86 pub id: Uuid,
87 #[schemars(title = "Порядковый номер уровня")]
88 pub level: i64,
89 #[schemars(
90 title = "Набор id битв",
91 description = "Набор битв на уровне",
92 schema_with = "fight_template_link_id_array_schema"
93 )]
94 pub fight_ids: Vec<FightTemplateId>,
95 #[schemars(title = "Название боя")]
96 #[translate]
97 pub title: i18n::I18nString,
98}
99
100#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Tsify, JsonSchema)]
101pub struct CharacterLevel {
102 #[schemars(title = "Уровень игрока")]
103 pub level: i64,
104 #[schemars(title = "Слотов способностей на уровне (для генерации оппонентов)")]
105 pub ability_slots: u64,
106 #[schemars(
107 title = "Минимальный опыт для получения",
108 description = "Минимальный опыт игрока для получения уровня"
109 )]
110 pub required_experience: i64,
111 #[schemars(
112 title = "Базовые аттрибуты игрока на уровне",
113 description = "Базовые аттрибуты игрока, которые выдаются на этом уровне"
114 )]
115 pub attributes: Vec<EntityAttribute>,
116}
117
118#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Tsify, JsonSchema)]
125pub struct CharacterLevelAttributeFormula {
126 #[schemars(schema_with = "attribute_link_id_schema")]
127 pub attribute_id: AttributeId,
128 #[schemars(title = "Значение атрибута на опорном уровне")]
129 pub anchor_value: f64,
130 #[schemars(title = "Коэффициент прироста атрибута")]
131 pub coefficient: f64,
132 #[schemars(
133 title = "Показатель степени прироста",
134 description = "Меньше 1 — затухающий прирост на уровень"
135 )]
136 pub exponent: f64,
137}
138
139#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Tsify, JsonSchema)]
154pub struct CharacterLevelFormula {
155 #[schemars(
156 title = "Опорный уровень",
157 description = "Последний авторский уровень; формула действует для уровней выше него"
158 )]
159 pub anchor_level: i64,
160 #[schemars(title = "Суммарный опыт на опорном уровне")]
161 pub anchor_experience: i64,
162 #[schemars(
163 title = "Стоимость первого уровня после опорного",
164 description = "Прирост требуемого опыта на уровне anchor_level+1"
165 )]
166 pub experience_marginal_base: i64,
167 #[schemars(
168 title = "Рост стоимости уровня",
169 description = "Геометрический множитель стоимости каждого следующего уровня до late_band_level"
170 )]
171 pub experience_marginal_growth: f64,
172 #[schemars(
173 title = "Уровень смены полосы роста",
174 description = "Последний уровень, растущий по experience_marginal_growth; выше действует experience_marginal_growth_late"
175 )]
176 pub experience_late_band_level: i64,
177 #[schemars(
178 title = "Рост стоимости уровня после смены полосы",
179 description = "Геометрический множитель стоимости уровня выше experience_late_band_level"
180 )]
181 pub experience_marginal_growth_late: f64,
182 #[schemars(
183 title = "Слотов способностей после опорного уровня",
184 description = "Постоянное значение — слоты не растут с бесконечными уровнями"
185 )]
186 pub ability_slots: u64,
187 #[schemars(title = "Формулы атрибутов на уровнях выше опорного")]
188 pub attributes: Vec<CharacterLevelAttributeFormula>,
189}
190
191fn geometric_sum(base: f64, g: f64, n: i64) -> f64 {
194 if n <= 0 {
195 return 0.0;
196 }
197 if (g - 1.0).abs() < f64::EPSILON {
198 base * n as f64
199 } else {
200 base * (g.powf(n as f64) - 1.0) / (g - 1.0)
201 }
202}
203
204impl CharacterLevelFormula {
205 pub fn required_experience(&self, level: i64) -> i64 {
212 let n = (level - self.anchor_level).max(0);
213 if n == 0 {
214 return self.anchor_experience;
215 }
216 let base = self.experience_marginal_base as f64;
217 let first_band = (self.experience_late_band_level - self.anchor_level).max(0);
218 let in_first = n.min(first_band);
219
220 let mut extra = geometric_sum(base, self.experience_marginal_growth, in_first);
221 if n > first_band {
222 let late_base = base
226 * self
227 .experience_marginal_growth
228 .powi((first_band - 1).max(0) as i32)
229 * self.experience_marginal_growth_late;
230 extra += geometric_sum(
231 late_base,
232 self.experience_marginal_growth_late,
233 n - first_band,
234 );
235 }
236
237 let total = self.anchor_experience as f64 + extra;
238 if total >= i64::MAX as f64 {
239 i64::MAX
240 } else {
241 total.round() as i64
242 }
243 }
244
245 pub fn level_for_experience(&self, experience: i64) -> i64 {
249 if experience <= self.anchor_experience {
250 return self.anchor_level;
251 }
252 let base = self.experience_marginal_base as f64;
253 let first_band = (self.experience_late_band_level - self.anchor_level).max(0);
254 let band_end_experience = self.required_experience(self.experience_late_band_level);
255
256 let (g, band_base, band_offset, surplus) = if experience <= band_end_experience {
259 (
260 self.experience_marginal_growth,
261 base,
262 0,
263 (experience - self.anchor_experience) as f64,
264 )
265 } else {
266 let late_base = base
267 * self
268 .experience_marginal_growth
269 .powi((first_band - 1).max(0) as i32)
270 * self.experience_marginal_growth_late;
271 (
272 self.experience_marginal_growth_late,
273 late_base,
274 first_band,
275 (experience - band_end_experience) as f64,
276 )
277 };
278 let n_approx = if (g - 1.0).abs() < f64::EPSILON {
279 band_offset as f64 + surplus / band_base
280 } else {
281 band_offset as f64 + (1.0 + surplus * (g - 1.0) / band_base).ln() / g.ln()
282 };
283 let mut n = n_approx.floor().max(0.0) as i64;
288 loop {
289 let next = self.required_experience(self.anchor_level + n + 1);
290 if next == i64::MAX || next > experience {
291 break;
292 }
293 n += 1;
294 }
295 while n > 0 && self.required_experience(self.anchor_level + n) > experience {
296 n -= 1;
297 }
298 self.anchor_level + n
299 }
300
301 pub fn attributes_for_level(&self, level: i64) -> Vec<EntityAttribute> {
303 let anchor = self.anchor_level as f64;
304 let l = level as f64;
305 self.attributes
306 .iter()
307 .map(|a| {
308 let value = if a.coefficient == 0.0 {
309 a.anchor_value
310 } else {
311 a.anchor_value + a.coefficient * (l.powf(a.exponent) - anchor.powf(a.exponent))
312 };
313 EntityAttribute {
314 attribute_id: a.attribute_id,
315 value: value.round().max(0.0) as u64,
316 }
317 })
318 .collect()
319 }
320
321 pub fn character_level(&self, level: i64) -> CharacterLevel {
323 CharacterLevel {
324 level,
325 ability_slots: self.ability_slots,
326 required_experience: self.required_experience(level),
327 attributes: self.attributes_for_level(level),
328 }
329 }
330}
331
332#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Tsify, JsonSchema)]
333pub struct AbilitySlotsLevel {
334 #[schemars(title = "Минимальный уровень главы")]
335 pub from_chapter_level: i64,
336 #[schemars(title = "Сколько слотов абилок открыто на уровне")]
337 pub ability_slots: u64,
338}
339
340#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Tsify, JsonSchema)]
341pub struct PetSlotsLevel {
342 #[schemars(title = "Минимальный уровень главы")]
343 pub from_chapter_level: i64,
344 #[schemars(title = "Сколько слотов петов открыто на уровне")]
345 pub pet_slots: u64,
346}
347
348#[cfg(test)]
349mod character_level_formula_tests {
350 use super::*;
351
352 fn shipped_formula() -> CharacterLevelFormula {
354 CharacterLevelFormula {
355 anchor_level: 100,
356 anchor_experience: 3_996_483,
357 experience_marginal_base: 204_604,
358 experience_marginal_growth: 1.03,
359 experience_late_band_level: 200,
360 experience_marginal_growth_late: 1.015,
361 ability_slots: 6,
362 attributes: vec![
363 CharacterLevelAttributeFormula {
364 attribute_id: uuid::Uuid::nil(),
365 anchor_value: 505.0,
366 coefficient: 100.0,
367 exponent: 0.5,
368 },
369 CharacterLevelAttributeFormula {
370 attribute_id: uuid::Uuid::from_u128(1),
371 anchor_value: 101.0,
372 coefficient: 20.0,
373 exponent: 0.5,
374 },
375 ],
376 }
377 }
378
379 #[test]
380 fn experience_is_continuous_and_monotonic_at_anchor() {
381 let f = shipped_formula();
382 assert_eq!(f.required_experience(100), 3_996_483);
384 assert_eq!(
386 f.required_experience(101) - f.required_experience(100),
387 204_604
388 );
389 let mut prev = f.required_experience(100);
393 for level in 101..=200 {
394 let cur = f.required_experience(level);
395 assert!(cur > prev, "exp must be strictly increasing at {level}");
396 prev = cur;
397 }
398 }
399
400 #[test]
404 fn the_late_band_slows_growth_without_a_step() {
405 let f = shipped_formula();
406 let marginal = |level: i64| f.required_experience(level) - f.required_experience(level - 1);
407
408 let last_early = marginal(200);
409 let first_late = marginal(201);
410 assert!(
411 (first_late as f64 - last_early as f64 * 1.015).abs() <= 1.0,
412 "the seam continues the curve: {last_early} -> {first_late}"
413 );
414
415 assert!((marginal(150) as f64 / marginal(149) as f64 - 1.03).abs() < 1e-3);
417 assert!((marginal(250) as f64 / marginal(249) as f64 - 1.015).abs() < 1e-3);
418
419 for (level, expected) in [
422 (200_i64, 128_249_255_i64),
423 (250, 413_764_313),
424 (300, 1_014_842_556),
425 ] {
426 let got = f.required_experience(level);
427 let drift = (got - expected).abs() as f64 / expected as f64;
428 assert!(drift < 1e-4, "L{level}: {got} vs {expected}");
429 }
430 }
431
432 #[test]
433 fn level_for_experience_inverts_required_experience() {
434 let f = shipped_formula();
435 for level in 101..=200 {
436 let exp = f.required_experience(level);
437 assert_eq!(f.level_for_experience(exp), level, "at level {level}");
439 assert_eq!(
441 f.level_for_experience(f.required_experience(level + 1) - 1),
442 level
443 );
444 }
445 }
446
447 #[test]
448 fn stats_diminish_but_keep_growing() {
449 let f = shipped_formula();
450 let a = f.attributes_for_level(100);
452 assert_eq!(a[0].value, 505);
453 assert_eq!(a[1].value, 101);
454 let a101 = f.attributes_for_level(101);
456 assert_eq!(a101[0].value, 510);
457 assert_eq!(a101[1].value, 102);
458 let d_early = f.attributes_for_level(120)[0].value - f.attributes_for_level(119)[0].value;
460 let d_late = f.attributes_for_level(400)[0].value - f.attributes_for_level(399)[0].value;
461 assert!(d_late < d_early, "late hp gain must be smaller than early");
462 assert!(d_late >= 1, "hp must keep growing");
463 }
464
465 #[test]
466 fn ability_slots_never_grow() {
467 let f = shipped_formula();
468 for level in [101, 200, 1000, 100_000] {
469 assert_eq!(f.character_level(level).ability_slots, 6);
470 }
471 }
472
473 #[test]
474 fn levels_are_unbounded() {
475 let f = shipped_formula();
476 let cl = f.character_level(200);
478 assert_eq!(cl.level, 200);
479 assert!(cl.required_experience > f.required_experience(199));
480 assert!(cl.attributes[0].value > 505);
481 let huge = f.character_level(100_000);
484 assert_eq!(huge.level, 100_000);
485 assert_eq!(huge.ability_slots, 6);
486 assert!(huge.attributes[0].value > cl.attributes[0].value);
487 assert_eq!(
488 f.level_for_experience(i64::MAX),
489 f.level_for_experience(i64::MAX)
490 );
491 }
492}