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(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Tsify, JsonSchema)]
82pub struct Chapter {
83 #[schemars(schema_with = "id_schema")]
84 pub id: Uuid,
85 #[schemars(title = "Порядковый номер уровня")]
86 pub level: i64,
87 #[schemars(
88 title = "Набор id битв",
89 description = "Набор битв на уровне",
90 schema_with = "fight_template_link_id_array_schema"
91 )]
92 pub fight_ids: Vec<FightTemplateId>,
93 #[schemars(title = "Название боя")]
94 pub title: i18n::I18nString,
95}
96
97#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Tsify, JsonSchema)]
98pub struct CharacterLevel {
99 #[schemars(title = "Уровень игрока")]
100 pub level: i64,
101 #[schemars(title = "Слотов способностей на уровне (для генерации оппонентов)")]
102 pub ability_slots: u64,
103 #[schemars(
104 title = "Минимальный опыт для получения",
105 description = "Минимальный опыт игрока для получения уровня"
106 )]
107 pub required_experience: i64,
108 #[schemars(
109 title = "Базовые аттрибуты игрока на уровне",
110 description = "Базовые аттрибуты игрока, которые выдаются на этом уровне"
111 )]
112 pub attributes: Vec<EntityAttribute>,
113}
114
115#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Tsify, JsonSchema)]
122pub struct CharacterLevelAttributeFormula {
123 #[schemars(schema_with = "attribute_link_id_schema")]
124 pub attribute_id: AttributeId,
125 #[schemars(title = "Значение атрибута на опорном уровне")]
126 pub anchor_value: f64,
127 #[schemars(title = "Коэффициент прироста атрибута")]
128 pub coefficient: f64,
129 #[schemars(
130 title = "Показатель степени прироста",
131 description = "Меньше 1 — затухающий прирост на уровень"
132 )]
133 pub exponent: f64,
134}
135
136#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Tsify, JsonSchema)]
151pub struct CharacterLevelFormula {
152 #[schemars(
153 title = "Опорный уровень",
154 description = "Последний авторский уровень; формула действует для уровней выше него"
155 )]
156 pub anchor_level: i64,
157 #[schemars(title = "Суммарный опыт на опорном уровне")]
158 pub anchor_experience: i64,
159 #[schemars(
160 title = "Стоимость первого уровня после опорного",
161 description = "Прирост требуемого опыта на уровне anchor_level+1"
162 )]
163 pub experience_marginal_base: i64,
164 #[schemars(
165 title = "Рост стоимости уровня",
166 description = "Геометрический множитель стоимости каждого следующего уровня до late_band_level"
167 )]
168 pub experience_marginal_growth: f64,
169 #[schemars(
170 title = "Уровень смены полосы роста",
171 description = "Последний уровень, растущий по experience_marginal_growth; выше действует experience_marginal_growth_late"
172 )]
173 pub experience_late_band_level: i64,
174 #[schemars(
175 title = "Рост стоимости уровня после смены полосы",
176 description = "Геометрический множитель стоимости уровня выше experience_late_band_level"
177 )]
178 pub experience_marginal_growth_late: f64,
179 #[schemars(
180 title = "Слотов способностей после опорного уровня",
181 description = "Постоянное значение — слоты не растут с бесконечными уровнями"
182 )]
183 pub ability_slots: u64,
184 #[schemars(title = "Формулы атрибутов на уровнях выше опорного")]
185 pub attributes: Vec<CharacterLevelAttributeFormula>,
186}
187
188fn geometric_sum(base: f64, g: f64, n: i64) -> f64 {
191 if n <= 0 {
192 return 0.0;
193 }
194 if (g - 1.0).abs() < f64::EPSILON {
195 base * n as f64
196 } else {
197 base * (g.powf(n as f64) - 1.0) / (g - 1.0)
198 }
199}
200
201impl CharacterLevelFormula {
202 pub fn required_experience(&self, level: i64) -> i64 {
209 let n = (level - self.anchor_level).max(0);
210 if n == 0 {
211 return self.anchor_experience;
212 }
213 let base = self.experience_marginal_base as f64;
214 let first_band = (self.experience_late_band_level - self.anchor_level).max(0);
215 let in_first = n.min(first_band);
216
217 let mut extra = geometric_sum(base, self.experience_marginal_growth, in_first);
218 if n > first_band {
219 let late_base = base
223 * self
224 .experience_marginal_growth
225 .powi((first_band - 1).max(0) as i32)
226 * self.experience_marginal_growth_late;
227 extra += geometric_sum(
228 late_base,
229 self.experience_marginal_growth_late,
230 n - first_band,
231 );
232 }
233
234 let total = self.anchor_experience as f64 + extra;
235 if total >= i64::MAX as f64 {
236 i64::MAX
237 } else {
238 total.round() as i64
239 }
240 }
241
242 pub fn level_for_experience(&self, experience: i64) -> i64 {
246 if experience <= self.anchor_experience {
247 return self.anchor_level;
248 }
249 let base = self.experience_marginal_base as f64;
250 let first_band = (self.experience_late_band_level - self.anchor_level).max(0);
251 let band_end_experience = self.required_experience(self.experience_late_band_level);
252
253 let (g, band_base, band_offset, surplus) = if experience <= band_end_experience {
256 (
257 self.experience_marginal_growth,
258 base,
259 0,
260 (experience - self.anchor_experience) as f64,
261 )
262 } else {
263 let late_base = base
264 * self
265 .experience_marginal_growth
266 .powi((first_band - 1).max(0) as i32)
267 * self.experience_marginal_growth_late;
268 (
269 self.experience_marginal_growth_late,
270 late_base,
271 first_band,
272 (experience - band_end_experience) as f64,
273 )
274 };
275 let n_approx = if (g - 1.0).abs() < f64::EPSILON {
276 band_offset as f64 + surplus / band_base
277 } else {
278 band_offset as f64 + (1.0 + surplus * (g - 1.0) / band_base).ln() / g.ln()
279 };
280 let mut n = n_approx.floor().max(0.0) as i64;
285 loop {
286 let next = self.required_experience(self.anchor_level + n + 1);
287 if next == i64::MAX || next > experience {
288 break;
289 }
290 n += 1;
291 }
292 while n > 0 && self.required_experience(self.anchor_level + n) > experience {
293 n -= 1;
294 }
295 self.anchor_level + n
296 }
297
298 pub fn attributes_for_level(&self, level: i64) -> Vec<EntityAttribute> {
300 let anchor = self.anchor_level as f64;
301 let l = level as f64;
302 self.attributes
303 .iter()
304 .map(|a| {
305 let value = if a.coefficient == 0.0 {
306 a.anchor_value
307 } else {
308 a.anchor_value + a.coefficient * (l.powf(a.exponent) - anchor.powf(a.exponent))
309 };
310 EntityAttribute {
311 attribute_id: a.attribute_id,
312 value: value.round().max(0.0) as u64,
313 }
314 })
315 .collect()
316 }
317
318 pub fn character_level(&self, level: i64) -> CharacterLevel {
320 CharacterLevel {
321 level,
322 ability_slots: self.ability_slots,
323 required_experience: self.required_experience(level),
324 attributes: self.attributes_for_level(level),
325 }
326 }
327}
328
329#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Tsify, JsonSchema)]
330pub struct AbilitySlotsLevel {
331 #[schemars(title = "Минимальный уровень главы")]
332 pub from_chapter_level: i64,
333 #[schemars(title = "Сколько слотов абилок открыто на уровне")]
334 pub ability_slots: u64,
335}
336
337#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Tsify, JsonSchema)]
338pub struct PetSlotsLevel {
339 #[schemars(title = "Минимальный уровень главы")]
340 pub from_chapter_level: i64,
341 #[schemars(title = "Сколько слотов петов открыто на уровне")]
342 pub pet_slots: u64,
343}
344
345#[cfg(test)]
346mod character_level_formula_tests {
347 use super::*;
348
349 fn shipped_formula() -> CharacterLevelFormula {
351 CharacterLevelFormula {
352 anchor_level: 100,
353 anchor_experience: 3_996_483,
354 experience_marginal_base: 204_604,
355 experience_marginal_growth: 1.03,
356 experience_late_band_level: 200,
357 experience_marginal_growth_late: 1.015,
358 ability_slots: 6,
359 attributes: vec![
360 CharacterLevelAttributeFormula {
361 attribute_id: uuid::Uuid::nil(),
362 anchor_value: 505.0,
363 coefficient: 100.0,
364 exponent: 0.5,
365 },
366 CharacterLevelAttributeFormula {
367 attribute_id: uuid::Uuid::from_u128(1),
368 anchor_value: 101.0,
369 coefficient: 20.0,
370 exponent: 0.5,
371 },
372 ],
373 }
374 }
375
376 #[test]
377 fn experience_is_continuous_and_monotonic_at_anchor() {
378 let f = shipped_formula();
379 assert_eq!(f.required_experience(100), 3_996_483);
381 assert_eq!(
383 f.required_experience(101) - f.required_experience(100),
384 204_604
385 );
386 let mut prev = f.required_experience(100);
390 for level in 101..=200 {
391 let cur = f.required_experience(level);
392 assert!(cur > prev, "exp must be strictly increasing at {level}");
393 prev = cur;
394 }
395 }
396
397 #[test]
401 fn the_late_band_slows_growth_without_a_step() {
402 let f = shipped_formula();
403 let marginal = |level: i64| f.required_experience(level) - f.required_experience(level - 1);
404
405 let last_early = marginal(200);
406 let first_late = marginal(201);
407 assert!(
408 (first_late as f64 - last_early as f64 * 1.015).abs() <= 1.0,
409 "the seam continues the curve: {last_early} -> {first_late}"
410 );
411
412 assert!((marginal(150) as f64 / marginal(149) as f64 - 1.03).abs() < 1e-3);
414 assert!((marginal(250) as f64 / marginal(249) as f64 - 1.015).abs() < 1e-3);
415
416 for (level, expected) in [
419 (200_i64, 128_249_255_i64),
420 (250, 413_764_313),
421 (300, 1_014_842_556),
422 ] {
423 let got = f.required_experience(level);
424 let drift = (got - expected).abs() as f64 / expected as f64;
425 assert!(drift < 1e-4, "L{level}: {got} vs {expected}");
426 }
427 }
428
429 #[test]
430 fn level_for_experience_inverts_required_experience() {
431 let f = shipped_formula();
432 for level in 101..=200 {
433 let exp = f.required_experience(level);
434 assert_eq!(f.level_for_experience(exp), level, "at level {level}");
436 assert_eq!(
438 f.level_for_experience(f.required_experience(level + 1) - 1),
439 level
440 );
441 }
442 }
443
444 #[test]
445 fn stats_diminish_but_keep_growing() {
446 let f = shipped_formula();
447 let a = f.attributes_for_level(100);
449 assert_eq!(a[0].value, 505);
450 assert_eq!(a[1].value, 101);
451 let a101 = f.attributes_for_level(101);
453 assert_eq!(a101[0].value, 510);
454 assert_eq!(a101[1].value, 102);
455 let d_early = f.attributes_for_level(120)[0].value - f.attributes_for_level(119)[0].value;
457 let d_late = f.attributes_for_level(400)[0].value - f.attributes_for_level(399)[0].value;
458 assert!(d_late < d_early, "late hp gain must be smaller than early");
459 assert!(d_late >= 1, "hp must keep growing");
460 }
461
462 #[test]
463 fn ability_slots_never_grow() {
464 let f = shipped_formula();
465 for level in [101, 200, 1000, 100_000] {
466 assert_eq!(f.character_level(level).ability_slots, 6);
467 }
468 }
469
470 #[test]
471 fn levels_are_unbounded() {
472 let f = shipped_formula();
473 let cl = f.character_level(200);
475 assert_eq!(cl.level, 200);
476 assert!(cl.required_experience > f.required_experience(199));
477 assert!(cl.attributes[0].value > 505);
478 let huge = f.character_level(100_000);
481 assert_eq!(huge.level, 100_000);
482 assert_eq!(huge.ability_slots, 6);
483 assert!(huge.attributes[0].value > cl.attributes[0].value);
484 assert_eq!(
485 f.level_for_experience(i64::MAX),
486 f.level_for_experience(i64::MAX)
487 );
488 }
489}