1use essences::items::{AttributeId, ItemRarityId};
2use schema_loader::{attribute_link_id_schema, item_rarity_link_id_schema};
3use schemars::JsonSchema;
4use serde::{Deserialize, Serialize};
5use tsify_next::Tsify;
6
7use crate::validated_types::NonEmptyVec;
8
9pub const MIN_PLINKO_ROWS: i64 = 2;
12pub const MAX_PLINKO_ROWS: i64 = 16;
13
14#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema, Tsify)]
20pub struct PlinkoSlotGroup {
21 #[schemars(title = "Первый физический слот группы")]
22 pub from_slot: i64,
23
24 #[schemars(title = "Последний физический слот группы")]
25 pub to_slot: i64,
26
27 #[schemars(
28 title = "Редкость предмета для этой группы слотов",
29 schema_with = "item_rarity_link_id_schema"
30 )]
31 pub rarity_id: ItemRarityId,
32
33 #[schemars(title = "Слот показан закрытым (только визуал)")]
34 pub is_locked: bool,
35}
36
37#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema, Tsify)]
45pub struct PlinkoLevelSlotLayout {
46 #[schemars(title = "Уровень сундука, для которого действует раскладка")]
47 pub chest_level: i64,
48
49 #[schemars(title = "Раскладка слотов доски на этом уровне (слот → редкость)")]
50 pub slot_groups: NonEmptyVec<PlinkoSlotGroup>,
51}
52
53impl PlinkoLevelSlotLayout {
54 pub fn rarity_for_slot(&self, slot: i64) -> Option<ItemRarityId> {
56 self.slot_groups
57 .iter()
58 .find(|group| slot >= group.from_slot && slot <= group.to_slot)
59 .map(|group| group.rarity_id)
60 }
61
62 pub fn slots_for_rarity(&self, rarity_id: ItemRarityId) -> Vec<i64> {
64 self.slot_groups
65 .iter()
66 .filter(|group| group.rarity_id == rarity_id)
67 .flat_map(|group| group.from_slot..=group.to_slot)
68 .collect()
69 }
70}
71
72#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema, Tsify)]
80pub struct PlinkoPinBonus {
81 #[schemars(title = "Ряд колышка (0 — верхний)")]
82 pub row: i64,
83
84 #[schemars(title = "Колонка колышка внутри ряда (0..=row)")]
85 pub column: i64,
86
87 #[schemars(
88 title = "Атрибут, который прибавляет колышек",
89 schema_with = "attribute_link_id_schema"
90 )]
91 pub attribute_id: AttributeId,
92
93 #[schemars(
94 title = "Базовый вес колышка",
95 description = "Безразмерное число. Реальная прибавка = вес × scale активного транша соответствующего уровня сундука."
96 )]
97 pub value: i64,
98}
99
100pub const PLINKO_MICRO: i64 = 1_000_000;
104
105#[derive(Clone, Copy, Debug, Deserialize, Serialize, JsonSchema, Tsify)]
112pub struct PlinkoAxisTranche {
113 #[schemars(title = "Ось (атрибут)", schema_with = "attribute_link_id_schema")]
114 pub attribute_id: AttributeId,
115
116 #[schemars(
117 title = "Потолок оси на этом уровне, микроединицы",
118 description = "Суммарно накопленный бонус оси не может превысить это значение, пока сундук на этом уровне."
119 )]
120 pub cap_micro: i64,
121
122 #[schemars(
123 title = "Цена единицы веса на этом уровне, микроединицы",
124 description = "Прибавка за касание = базовый вес колышка × это значение."
125 )]
126 pub scale_micro: i64,
127}
128
129#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema, Tsify)]
131pub struct PlinkoTranche {
132 #[schemars(title = "Уровень сундука")]
133 pub chest_level: i64,
134
135 #[schemars(
136 title = "Оси уровня",
137 description = "По одной строке на каждую ось таблицы колышков."
138 )]
139 pub axes: Vec<PlinkoAxisTranche>,
140}
141
142#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema, Tsify)]
149pub struct PlinkoSettings {
150 #[schemars(
151 title = "Высота доски (рядов колышков)",
152 description = "Физических слотов будет rows + 1, колышков — rows*(rows+1)/2. Путь шарика состоит ровно из rows шагов."
153 )]
154 pub rows: i64,
155
156 #[schemars(
157 title = "Раскладки слотов доски по уровням сундука",
158 description = "По одной раскладке на каждый уровень item_cases_settings. Раскладка уровня обязана покрывать все редкости, выпадающие на этом уровне."
159 )]
160 pub slot_layouts: NonEmptyVec<PlinkoLevelSlotLayout>,
161
162 #[schemars(
163 title = "Бонусы за колышки (ПЛЕЙСХОЛДЕРЫ)",
164 description = "Значения не отбалансированы: симуляция не проводилась. Колышки без записи не дают ничего."
165 )]
166 pub pin_bonuses: Vec<PlinkoPinBonus>,
167
168 #[schemars(
169 title = "Траншы потолков и шагов по уровням сундука",
170 description = "По одной строке на уровень сундука; каждая ось таблицы колышков обязана присутствовать в каждой строке. Потолки не убывают с уровнем."
171 )]
172 pub tranches: Vec<PlinkoTranche>,
173}
174
175impl PlinkoSettings {
176 pub fn slot_count(&self) -> i64 {
178 self.rows + 1
179 }
180
181 pub fn layout_for_level(&self, chest_level: i64) -> Option<&PlinkoLevelSlotLayout> {
185 self.slot_layouts
186 .iter()
187 .find(|layout| layout.chest_level == chest_level)
188 }
189
190 pub fn pin_bonus(&self, row: i64, column: i64) -> Option<&PlinkoPinBonus> {
192 self.pin_bonuses
193 .iter()
194 .find(|bonus| bonus.row == row && bonus.column == column)
195 }
196
197 pub fn tranche(
199 &self,
200 chest_level: i64,
201 attribute_id: AttributeId,
202 ) -> Option<&PlinkoAxisTranche> {
203 self.tranches
204 .iter()
205 .find(|tranche| tranche.chest_level == chest_level)
206 .and_then(|tranche| {
207 tranche
208 .axes
209 .iter()
210 .find(|axis| axis.attribute_id == attribute_id)
211 })
212 }
213
214 pub fn active_tranche(
224 &self,
225 chest_level: i64,
226 attribute_id: AttributeId,
227 current_micro: i64,
228 ) -> Option<&PlinkoAxisTranche> {
229 let mut previous_cap = 0;
230 let mut fallback: Option<&PlinkoAxisTranche> = None;
231 for level in 1..=chest_level {
232 let Some(axis) = self.tranche(level, attribute_id) else {
233 continue;
234 };
235 let adds_capacity = axis.cap_micro > previous_cap;
236 previous_cap = axis.cap_micro.max(previous_cap);
237 if adds_capacity && axis.cap_micro > current_micro {
238 return Some(axis);
239 }
240 fallback = Some(axis);
241 }
242 fallback
246 }
247
248 pub fn cap_micro_at(&self, chest_level: i64, attribute_id: AttributeId) -> i64 {
251 (1..=chest_level)
252 .filter_map(|level| self.tranche(level, attribute_id))
253 .map(|axis| axis.cap_micro)
254 .max()
255 .unwrap_or(0)
256 }
257
258 pub fn whole_from_micro(micro: i64) -> i64 {
260 micro.div_euclid(PLINKO_MICRO)
261 }
262}