configs/
artifacts.rs

1//! Artifact configuration — the two catalogs and every tunable the runtime
2//! reads.
3//!
4//! Everything a designer can retune from the LiveOps panel lives here: the
5//! ownership stat bonus per artifact level, the duplicate ladder, the socket
6//! unlock schedule, the artifact-stone upgrade ladder, and the (placeholder)
7//! acquisition sources.
8//!
9//! **Every number in this module is a placeholder.** The design doc leaves the
10//! ownership percentages, the unlock chapters, the drop chances and the stone
11//! upgrade step explicitly unassigned (§10), so they are config, not constants,
12//! and the shipped values exist only so the system can be exercised end to end.
13
14use essences::artifacts::{
15    ArtifactSocketColumn, ArtifactSocketSlot, ArtifactStoneTemplateId, ArtifactTemplateId,
16};
17use essences::items::AttributeId;
18use essences::stones::StoneTier;
19use schema_loader::attribute_link_id_schema;
20use schemars::JsonSchema;
21use serde::{Deserialize, Serialize};
22use tsify_next::Tsify;
23
24/// The one-line rule welded into an artifact, active only while it is worn.
25///
26/// A World Law reclassifies what the build already produces; it never adds a
27/// number (design §4). It is the only thing allowed to touch the gauge, the
28/// Core/Proc boundary, the flip law or the socket limits — the Aspect stones
29/// cannot.
30///
31/// The v0.2 pool is three artifacts: `ART-00 Plain Gate Key` (the starter
32/// «Пустая оправа», no law at all), `ART-03 Halfway Bell` — the only law that
33/// ships here — and `ART-02 Thin Mirror`, which reads hidden Laws and therefore
34/// waits for the cores/laws vertical. «Отражение» (a dodge additionally reading
35/// as a crit) was cut from the pool in v0.2: it is a rulebreaker, and those live
36/// in a separate `Artifacts v2` wave.
37///
38/// Variant docs live here rather than on the variants themselves: a documented
39/// variant makes the admin schema generate a `oneOf` of one-value enums instead
40/// of a plain picker.
41///
42/// * `HalfwayBell` — «Колокол на полпути» (`ART-03`): the Global Flip happens at
43///   a *share* of the configured threshold
44///   ([`ArtifactsSettings::halfway_bell_threshold_share`], `60%`). No other
45///   gauge source is amplified — the bar moves, the fill rate does not.
46/// * `ThinMirror` — «Тонкое зеркало» (`ART-02`): EVERY slotted law of the side
47///   that is currently down keeps evaluating its own Core conditions and, when
48///   one is met, applies its Effect at
49///   [`ArtifactsSettings::thin_mirror_share`] (`30%`) of Law Power. It is the
50///   most expensive rule in the pool — it switches the other half of the build
51///   back on — and the whole of its safety is the fourth clause of the
52///   intersection rule (design §3): a hidden activation produces NO Resonance,
53///   NO bridge charge and NO Flip Gauge, and its Effect is not a Core event.
54///   Without that clause it would double both the bridge fill and the flip
55///   tempo.
56#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, Hash, JsonSchema, Tsify)]
57#[tsify(from_wasm_abi, into_wasm_abi)]
58pub enum ArtifactWorldLaw {
59    HalfwayBell,
60    ThinMirror,
61}
62
63/// Which rule an artifact stone carries.
64///
65/// Five per socket (design §5) — a socket is a choice between readings of one
66/// moment, never between a small number and a big one. Rank scales the number
67/// and opens no new mechanic, which is why a level moves
68/// [`ArtifactStoneTemplate::magnitude`] and nothing else.
69///
70/// **Twenty-nine rules, not thirty.** The left (Aspect) column has fifteen and
71/// retunes the Trigger/Effect stones worn in gear; the right (Law) column has
72/// **fourteen** and retunes the laws and bridges worn in cores. The Bridge Law
73/// socket ships four: `BL-04 Priority Gate` was cut because it gave `+50%`
74/// Resonance "from its own source", which is a law-shaped effect in a bridge
75/// socket and double-covered the rewritten `BL-05` (post-merge plan §8). The
76/// numbering keeps the hole — `BL-01`, `BL-02`, `BL-03`, `BL-05` — as the
77/// catalog already does for the missing `ART-01`.
78///
79/// Variant docs live here rather than on the variants themselves: a documented
80/// variant makes the admin schema generate a `oneOf` of one-value enums instead
81/// of a plain picker.
82///
83/// Visible Aspect — how active gear effects behave:
84///
85/// * `Crescendo` (`VA-01`) — active effects ramp linearly with the gauge, from
86///   `secondary_magnitude`% on an empty gauge to `magnitude`% just before the
87///   flip (`85% → 130%`).
88/// * `Alternator` (`VA-02`) — a proc of a *different* gear slot runs at
89///   `magnitude`%, a repeat of the previous slot at `secondary_magnitude`%
90///   (`125% / 90%`). "Previous" means the previously resolved slot, in the
91///   `ItemType::iter()` order every slot rule shares.
92/// * `FocusedEngine` (`VA-03`) — repeating one slot adds `magnitude`% per
93///   repeat up to `secondary_magnitude`% (`+10%` per repeat, `+30%` cap);
94///   a proc of another slot resets the streak.
95/// * `OpeningFive` (`VA-04`) — the first proc of each of the five slots in a
96///   phase runs at `magnitude`% (`150%`), the rest are plain.
97/// * `SmallGears` (`VA-05`) — effects run at a share that depends on the tier of
98///   the **trigger** that lit them
99///   ([`ArtifactStoneTemplate::tier_magnitudes`]: `130 / 115 / 105 / 100`).
100///
101/// Hidden Aspect — what hidden gear effects do:
102///
103/// * `BackgroundVoice` (`HA-01`) — every trigger runs the active effect in full
104///   and the paired hidden one at `magnitude`% (`30%`).
105/// * `ShadowRound` (`HA-02`) — once every `rule_param` ticks the next hidden
106///   effect in slot order fires by itself at `magnitude`% (`60%`).
107/// * `CrossRelay` (`HA-03`) — a proc of slot `N` additionally runs the hidden
108///   effect of slot `N+1` at `magnitude`% (`35%`).
109/// * `SecondHalf` (`HA-04`) — past `secondary_magnitude`% of the gauge, the
110///   paired hidden effect accompanies every proc at `magnitude`% (`50%`).
111/// * `CatchUp` (`HA-05`) — when the active effect is wholly inapplicable, the
112///   paired hidden one runs at `magnitude`% (`100%`) instead.
113///
114/// Flip Aspect — the transition itself:
115///
116/// * `IncomingSalvo` (`FA-01`) — after a flip the five newly active effects each
117///   run once at `magnitude`% (`50%`), all on the flip tick.
118/// * `LastWord` (`FA-02`) — the five *departing* active effects each run once at
119///   `magnitude`% (`50%`) as the flip happens.
120/// * `WarmStart` (`FA-03`) — the first ordinary fire of each newly active effect
121///   is repeated at `magnitude`% (`60%`).
122/// * `Afterimage` (`FA-04`) — the first `rule_param` trigger procs after a flip
123///   are accompanied by the departing paired effect at `magnitude`% (`25%`).
124/// * `StaggeredEntrance` (`FA-05`) — the same five new effects as
125///   `IncomingSalvo`, at `magnitude`% (`70%`), spaced `rule_param` ticks apart
126///   in slot order.
127///
128/// Visible Law — the laws of the side that is currently UP. Each of these but
129/// `FirstArticle` acts on ONE law the player chose
130/// ([`essences::artifacts::ArtifactStone::law_target`]); a stone with no chosen
131/// law does nothing. Because the choice is a LAW and not a role, and a law's
132/// side is fixed, `SourceFocus` / `ExportPower` / `FullConfidence` only speak in
133/// their law's own phase and are silent for the other half of the fight — that
134/// half is what pays for the size of the bonus (post-merge plan §8):
135///
136/// * `SourceFocus` (`VL-01`) — the chosen law's Resonance `+magnitude`%
137///   (`+50%`), its Effect `secondary_magnitude`% (`−15%`). The catalog's "only
138///   into its own bridge" is dropped: a law's Resonance has no other address.
139/// * `CurrentPower` (`VL-02`) — the chosen law's Effect `+magnitude`% (`+35%`),
140///   its Resonance `secondary_magnitude`% (`−25%`). The mirror of `VL-01`.
141/// * `ExportPower` (`VL-03`) — the hard version of `VL-01`: Resonance
142///   `+magnitude`% (`+75%`), Effect `secondary_magnitude`% (`−25%`). Kept beside
143///   `VL-01` deliberately — same trade, two settings of it.
144/// * `FullConfidence` (`VL-04`) — once the chosen law has filled its outgoing
145///   bridge, that law runs at `+magnitude`% Effect (`+30%`) until the flip.
146/// * `FirstArticle` (`VL-05`) — the one rule of the column that addresses the
147///   whole build: the FIRST activation of EVERY law of the active side in a
148///   phase applies its Effect at `magnitude`% (`150%`), Resonance untouched. It
149///   changes the rhythm of a phase rather than the strength of one law, which is
150///   why it takes no target.
151///
152/// Hidden Law — the laws of the side that is currently DOWN. A hidden
153/// activation never produces Resonance, bridge charge or Flip Gauge, and its
154/// Effect is not a Core event ([`crate::artifacts::ArtifactWorldLaw::ThinMirror`]
155/// carries the same rule):
156///
157/// * `ProxyReader` (`HL-01`) — the chosen hidden law evaluates its own Core
158///   conditions and applies its Effect at `magnitude`% (`30%`).
159/// * `LinkedEcho` (`HL-02`) — when the chosen law activates, the law at the
160///   far end of its bridge applies its Effect at `magnitude`% (`20%`) without
161///   checking its own condition at all.
162/// * `LateAwakening` (`HL-03`) — past `secondary_magnitude`% of the gauge
163///   (`70%`) the chosen hidden law works at `magnitude`% (`50%`).
164/// * `SingleLesson` (`HL-04`) — the first Core event of a phase that satisfies
165///   the chosen hidden law wakes it at `magnitude`% (`100%`); the stone is then
166///   silent until the flip.
167/// * `ReadyTarget` (`HL-05`) — once the chosen law's bridge is full, the law at
168///   its far end works at `magnitude`% (`35%`) until the flip.
169///
170/// Bridge Law — the bridges themselves. `BL-01`…`BL-03` move EVERY bridge;
171/// `BL-05` is the only one that addresses a single bridge, through the law that
172/// sits in it:
173///
174/// * `ExtraSpan` (`BL-01`) — `rule_param` extra bridges over the
175///   `min(core level) − 1` budget, and every bridge's capacity `+magnitude`%
176///   (`+20%`).
177/// * `DeepSpan` (`BL-02`) — every bridge's capacity `+magnitude`% and its
178///   recipient amplification `+secondary_magnitude`% (`+40% / +40%`): a deeper
179///   bridge and a higher ceiling at the same price per unit.
180/// * `ShortSpan` (`BL-03`) — capacity `magnitude`% and amplification
181///   `secondary_magnitude`% (`−40% / −20%`). The minus signs are the design: a
182///   shorter bridge with a lower ceiling makes each unit worth MORE
183///   (`40% / 6 units` against `50% / 10`), so the cap arrives in three fires
184///   instead of five.
185/// * `ReciprocalGate` (`BL-05`) — `+magnitude`% Resonance (`+25%`) to BOTH
186///   directions of the chosen law's bridge, unconditionally. The catalog's
187///   original wording ("if both links A→B and B→A are chosen") describes a
188///   choice no version of the model offers — a bridge is one object carrying two
189///   directions — so it was rewritten (post-merge plan §8).
190#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, Hash, JsonSchema, Tsify)]
191#[tsify(from_wasm_abi, into_wasm_abi)]
192pub enum ArtifactStoneRule {
193    Crescendo,
194    Alternator,
195    FocusedEngine,
196    OpeningFive,
197    SmallGears,
198    BackgroundVoice,
199    ShadowRound,
200    CrossRelay,
201    SecondHalf,
202    CatchUp,
203    IncomingSalvo,
204    LastWord,
205    WarmStart,
206    Afterimage,
207    StaggeredEntrance,
208    SourceFocus,
209    CurrentPower,
210    ExportPower,
211    FullConfidence,
212    FirstArticle,
213    ProxyReader,
214    LinkedEcho,
215    LateAwakening,
216    SingleLesson,
217    ReadyTarget,
218    ExtraSpan,
219    DeepSpan,
220    ShortSpan,
221    ReciprocalGate,
222}
223
224impl ArtifactStoneRule {
225    /// The socket this rule belongs in. A stone's template names its socket
226    /// explicitly, and `GameConfig::validate_artifacts` checks the two agree —
227    /// so a mistyped catalog entry cannot ship a Flip rule into a Visible
228    /// socket.
229    pub const fn socket(self) -> ArtifactSocketSlot {
230        match self {
231            Self::Crescendo
232            | Self::Alternator
233            | Self::FocusedEngine
234            | Self::OpeningFive
235            | Self::SmallGears => ArtifactSocketSlot::VisibleAspect,
236            Self::BackgroundVoice
237            | Self::ShadowRound
238            | Self::CrossRelay
239            | Self::SecondHalf
240            | Self::CatchUp => ArtifactSocketSlot::HiddenAspect,
241            Self::IncomingSalvo
242            | Self::LastWord
243            | Self::WarmStart
244            | Self::Afterimage
245            | Self::StaggeredEntrance => ArtifactSocketSlot::FlipAspect,
246            Self::SourceFocus
247            | Self::CurrentPower
248            | Self::ExportPower
249            | Self::FullConfidence
250            | Self::FirstArticle => ArtifactSocketSlot::VisibleLaw,
251            Self::ProxyReader
252            | Self::LinkedEcho
253            | Self::LateAwakening
254            | Self::SingleLesson
255            | Self::ReadyTarget => ArtifactSocketSlot::HiddenLaw,
256            Self::ExtraSpan | Self::DeepSpan | Self::ShortSpan | Self::ReciprocalGate => {
257                ArtifactSocketSlot::BridgeLaw
258            }
259        }
260    }
261
262    /// Whether this rule is worthless until the player points its stone at a
263    /// law. Every Law-column rule but `VL-05 First Article` (which addresses the
264    /// whole build) and `BL-01`…`BL-03` (which move every bridge) needs one.
265    pub const fn needs_law_target(self) -> bool {
266        matches!(
267            self,
268            Self::SourceFocus
269                | Self::CurrentPower
270                | Self::ExportPower
271                | Self::FullConfidence
272                | Self::ProxyReader
273                | Self::LinkedEcho
274                | Self::LateAwakening
275                | Self::SingleLesson
276                | Self::ReadyTarget
277                | Self::ReciprocalGate
278        )
279    }
280
281    /// Whether this rule can wake a law of the side that is currently DOWN.
282    /// Those activations go through the intersection rule (design §3) and are
283    /// the only law fires that produce no Resonance and no bridge charge.
284    pub const fn wakes_hidden_law(self) -> bool {
285        matches!(
286            self,
287            Self::ProxyReader
288                | Self::LinkedEcho
289                | Self::LateAwakening
290                | Self::SingleLesson
291                | Self::ReadyTarget
292        )
293    }
294
295    /// Why this rule cannot run on this branch yet, or `None` when it can.
296    ///
297    /// Every rule now has the mechanic it reads, so nothing is blocked: the
298    /// left column's last three dependencies landed with the stones v0.2 merge,
299    /// and the whole right column's — laws, bridges, Resonance — landed with the
300    /// cores vertical. How the left column's were settled is worth keeping,
301    /// because the answers are the rules' semantics rather than incidental
302    /// history:
303    ///
304    /// * `Alternator` and `FocusedEngine` both say "another slot" / "a repeat of
305    ///   the previous slot", and the shared Trigger Cooldown lets up to five
306    ///   slots answer one Core event at once. **Resolved**: slots resolve in the
307    ///   fixed `ItemType::iter()` order (Weapon → Torso → Head → Gloves →
308    ///   Shoulders), the order `HA-03 Cross Relay` already ships on, and "the
309    ///   previous slot" is the previously *resolved* one, carried across event
310    ///   boundaries. The two alternatives were rejected on their merits: "the
311    ///   last slot of the previous batch" makes the bonus depend on batch
312    ///   boundaries the player cannot see, and "only when exactly one slot fired"
313    ///   switches both rules off for any five-socket build — the build they
314    ///   exist for.
315    /// * `SmallGears` keys off trigger tiers up to Legendary. **Resolved**: the
316    ///   stones v0.2 catalog ships Legendary triggers.
317    ///
318    /// `GameConfig::validate_artifacts` refuses a catalog entry that ships a
319    /// blocked rule with `active: true`. With nothing blocked the guard has no
320    /// work to do, and it stays because the next rule to be entered ahead of its
321    /// dependency will need it again.
322    pub const fn unavailable_dependency(self) -> Option<&'static str> {
323        None
324    }
325}
326
327/// One rung of `VA-05 Small Gears`: how strongly effects run when the trigger
328/// that fired them has this tier.
329///
330/// Carried on the template rather than hard-coded so the rule's whole content is
331/// visible in the catalog while it waits for the Legendary tier to exist.
332#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, Tsify)]
333#[tsify(from_wasm_abi, into_wasm_abi)]
334pub struct ArtifactTierMagnitude {
335    #[schemars(title = "Тир триггера")]
336    pub tier: StoneTier,
337
338    #[schemars(
339        title = "Сила эффекта, %",
340        description = "100 — обычная сила. Используется только правилом «Мелкие шестерни»."
341    )]
342    pub magnitude: f64,
343}
344
345/// Attribute codes whose `.mod` key is actually COMPOSED by the runtime.
346///
347/// An ownership bonus is written as `<code>.mod`, and only these codes are read
348/// back — combat composes them in `mechanics::fight::get_entity_stat` (plus `hp`
349/// through `attributes::compose_max_hp`) and the honest power scalar in
350/// `mechanics::balance::get_attr_from_attrs`. A bonus on any other attribute
351/// would be written, stored, shown in no stat and read by nothing: silently
352/// inert content. `GameConfig::validate_artifacts` refuses it instead.
353///
354/// This list lives here, not in `overlord_event_system`, because `configs` is
355/// the lower crate — the dependency only runs one way, so it cannot be derived
356/// from those functions and must be kept in sync with them by hand.
357///
358/// `game_config::tests::mod_capable_codes_cover_the_attributes_the_runtime_composes`
359/// pins the three codes the shipped catalog actually uses for ownership bonuses
360/// and the list's own shape (no duplicates, base codes rather than `.mod` keys).
361/// It deliberately does not assert full coverage: the direction that hurts is a
362/// code listed here that nothing reads (which would let inert content ship), and
363/// a code read by the runtime but missing here only costs a designer the ability
364/// to use it — a visible refusal, not a silent no-op.
365pub const MOD_CAPABLE_ATTRIBUTE_CODES: [&str; 14] = [
366    "armor",
367    "attack",
368    "block",
369    "bravery",
370    "counterattack_chance",
371    "crit_chance",
372    "crit_modifier",
373    "deceit",
374    "evasion",
375    "hp",
376    "multicast_chance",
377    "received_damage",
378    "regeneration_rate",
379    "speed",
380];
381
382/// One stat an artifact pays for being OWNED, in percent of the composed stat.
383///
384/// Mirrors the `StoneStat` idiom (`value` + `value_per_level`) so the two
385/// collections scale the same way and a reader who knows one knows the other.
386/// The difference is the unit: a stone stat is a flat attribute delta, this is a
387/// PERCENT written into `<code>.mod`.
388///
389/// An artifact carries a whole list of these, so "5% hp + 10% armor" is one
390/// template — the shared three-column table this replaced could only ever
391/// express one artifact-wide row per level.
392#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, Tsify)]
393#[tsify(from_wasm_abi, into_wasm_abi)]
394pub struct ArtifactOwnershipBonusStat {
395    #[schemars(title = "Id атрибута", schema_with = "attribute_link_id_schema")]
396    pub attribute_id: AttributeId,
397
398    #[schemars(
399        title = "Прибавка на 1 уровне, %",
400        description = "Проценты к атрибуту за владение артефактом 1 уровня."
401    )]
402    pub percent: f64,
403
404    #[schemars(
405        title = "Прибавка за каждый уровень выше первого, %",
406        description = "Итог = процент + процент_за_уровень * (уровень - 1)."
407    )]
408    pub percent_per_level: f64,
409}
410
411impl ArtifactOwnershipBonusStat {
412    /// The percent this stat pays on an artifact of `level`. Level 1 (and any
413    /// nonsense below it) is the base percent.
414    pub fn percent_at_level(&self, level: i64) -> f64 {
415        let levels_above_first = if level > 1 { level - 1 } else { 0 };
416        self.percent + self.percent_per_level * levels_above_first as f64
417    }
418}
419
420/// One entry of the artifact catalog.
421#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Tsify)]
422#[tsify(from_wasm_abi, into_wasm_abi)]
423pub struct ArtifactTemplate {
424    #[schemars(schema_with = "schema_loader::id_schema")]
425    pub id: ArtifactTemplateId,
426
427    #[schemars(title = "Название")]
428    pub name: i18n::I18nString,
429
430    #[schemars(title = "Описание")]
431    pub description: i18n::I18nString,
432
433    /// The welded-in rule, or `None` for an artifact that is a bare frame.
434    #[schemars(
435        title = "World Law",
436        description = "Правило, вшитое в артефакт. Работает только пока артефакт надет. Пусто — артефакт без правила (стартовая «Пустая оправа»)."
437    )]
438    pub world_law: Option<ArtifactWorldLaw>,
439
440    /// What owning this artifact pays, per attribute. Empty = pays nothing.
441    ///
442    /// Fully per-template: any set of attributes, any values. Every owned
443    /// artifact pays — worn or not — which is the one thing that makes a second
444    /// artifact worth having once only one can be worn.
445    #[schemars(
446        title = "Бонусы за владение",
447        description = "Список атрибутов и процентов, которые артефакт даёт за владение. Пусто — артефакт не даёт статов."
448    )]
449    pub ownership_bonuses: Vec<ArtifactOwnershipBonusStat>,
450
451    #[schemars(
452        title = "Иконка",
453        schema_with = "schema_loader::asset_artifact_icon_schema"
454    )]
455    pub icon_path: String,
456
457    #[schemars(
458        title = "Множитель Power World Law",
459        description = "Приблизительный вклад World Law этого артефакта в displayed Power, пока артефакт надет (BAL-030). От ранга не зависит; 1.0 у артефакта без правила."
460    )]
461    pub power_q: f64,
462}
463
464/// One entry of the artifact-stone catalog.
465///
466/// The first copy carries the whole rule; `magnitude_per_level` is the only
467/// thing an upgrade moves (design §3), which is why the rule and `rule_param`
468/// are level-independent.
469#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Tsify)]
470#[tsify(from_wasm_abi, into_wasm_abi)]
471pub struct ArtifactStoneTemplate {
472    #[schemars(
473        title = "Множитель Power на первом ранге",
474        description = "Приблизительный вклад в displayed/matchmaking Power (BAL-030). 1.0 = не влияет. Промежуточные ранги интерполируются линейно по ln(q)."
475    )]
476    pub power_q_first_rank: f64,
477
478    #[schemars(
479        title = "Множитель Power на максимальном ранге",
480        description = "Значение того же множителя на последнем ранге. Должен быть не меньше значения на первом."
481    )]
482    pub power_q_max_rank: f64,
483
484    #[schemars(schema_with = "schema_loader::id_schema")]
485    pub id: ArtifactStoneTemplateId,
486
487    #[schemars(title = "Название")]
488    pub name: i18n::I18nString,
489
490    #[schemars(title = "Описание")]
491    pub description: i18n::I18nString,
492
493    #[schemars(title = "Сокет")]
494    pub socket: ArtifactSocketSlot,
495
496    #[schemars(title = "Правило")]
497    pub rule: ArtifactStoneRule,
498
499    #[schemars(
500        title = "Базовая величина, %",
501        description = "Величина правила на 1-м уровне, в процентах. Правило целиком даётся первой копией; уровень двигает только это число."
502    )]
503    pub magnitude: f64,
504
505    #[schemars(title = "Прибавка величины за уровень прокачки, %")]
506    pub magnitude_per_level: f64,
507
508    #[schemars(
509        title = "Вторая величина правила, %",
510        description = "Второй процент правила: нижняя точка разгона у «Крещендо», сила повтора у «Чередования», потолок у «Сфокусированного мотора», доля шкалы у «Второй половины». Остальным правилам не нужна (0). Прокачкой не меняется."
511    )]
512    pub secondary_magnitude: f64,
513
514    #[schemars(
515        title = "Параметр правила",
516        description = "Период в тиках для «Теневого круга» и «Поочерёдного входа», число срабатываний для «Послеобраза»; для остальных правил не используется (0). Прокачкой не меняется."
517    )]
518    pub rule_param: i64,
519
520    #[schemars(
521        title = "Сила по тирам триггера",
522        description = "Только для «Мелких шестерён» (VA-05). У остальных камней список пустой."
523    )]
524    pub tier_magnitudes: Vec<ArtifactTierMagnitude>,
525
526    #[schemars(
527        title = "Правило включено",
528        description = "Выключенный камень остаётся в каталоге со своими числами, но рантайм его не исполняет. Так заведены правила, которым нужна ещё не отгруженная механика."
529    )]
530    pub active: bool,
531
532    #[schemars(
533        title = "Иконка",
534        schema_with = "schema_loader::asset_artifact_stone_icon_schema"
535    )]
536    pub icon_path: String,
537}
538
539impl ArtifactStoneTemplate {
540    /// The rule's magnitude on a stone of upgrade level `level`, in percent.
541    /// Same shape as `configs::stones::StoneStat::value_at_level`, so both
542    /// systems scale with a level identically.
543    pub fn magnitude_at_level(&self, level: i64) -> f64 {
544        let levels_above_first = (level - 1).max(0) as f64;
545        self.magnitude + self.magnitude_per_level * levels_above_first
546    }
547
548    /// `VA-05 Small Gears` only: the share effects run at when the trigger that
549    /// fired them had `tier`. An unlisted tier reads as plain strength rather
550    /// than as zero, so a half-filled table can never silence a build.
551    pub fn tier_magnitude(&self, tier: StoneTier) -> f64 {
552        self.tier_magnitudes
553            .iter()
554            .find(|entry| entry.tier == tier)
555            .map_or(100.0, |entry| entry.magnitude)
556    }
557}
558
559/// Ownership stat bonus of ONE artifact at a given level, in percent.
560///
561/// One rung of an upgrade ladder: how many raw copies buy `level`.
562///
563/// Used by both ladders — artifacts and artifact stones. In both cases a
564/// duplicate banks a raw copy and an explicit player action spends the rung.
565#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, Tsify)]
566#[tsify(from_wasm_abi, into_wasm_abi)]
567pub struct ArtifactUpgradeStep {
568    #[schemars(
569        title = "Уровень, который покупается",
570        description = "Лестница начинается со 2-го уровня и должна доходить до максимального без пропусков."
571    )]
572    pub level: i64,
573
574    #[schemars(
575        title = "Сколько сырых копий нужно",
576        description = "Сам предмет в это число не входит. Копии считаются штуками."
577    )]
578    pub copies: i64,
579}
580
581/// When one artifact socket opens.
582///
583/// The order is fixed by design — the whole Aspect column before the Law column
584/// — and `GameConfig::validate_artifacts` enforces it.
585///
586/// The three Law sockets used to be scheduled past the end of the campaign,
587/// because their stones had no laws to retune. The cores vertical shipped, so
588/// they carry real chapters now; the shipped schedule continues the left
589/// column's four-chapter cadence (see `artifacts_settings/_data.yaml` for the
590/// numbers and the reasoning).
591#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, Tsify)]
592#[tsify(from_wasm_abi, into_wasm_abi)]
593pub struct ArtifactSocketUnlock {
594    #[schemars(title = "Сокет")]
595    pub socket: ArtifactSocketSlot,
596
597    #[schemars(
598        title = "Глава разблокировки",
599        description = "BAL-007: подписанные главы 91/111/131/151/171/191. Порядок фиксирован: сначала вся левая колонка (Aspect), потом правая (Law)."
600    )]
601    pub unlock_chapter: i64,
602}
603
604/// BAL-031 source: where artifact stones drop from (signed rates).
605///
606/// Deliberately a different faucet from the artifacts themselves (design §5), so
607/// the two collections never compete for the same drop.
608#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, Tsify)]
609#[tsify(from_wasm_abi, into_wasm_abi)]
610pub struct ArtifactStoneDropSettings {
611    #[schemars(
612        title = "Шанс камня артефакта с босса главы",
613        description = "Доля в [0,1]. BAL-031: подписанное значение 0.02."
614    )]
615    pub chapter_boss_chance: f64,
616
617    #[schemars(
618        title = "Шанс камня артефакта за пройденное подземелье",
619        description = "Доля в [0,1]. BAL-031: подписанное значение 0.15."
620    )]
621    pub dungeon_chance: f64,
622
623    /// The third faucet, added by the acquisition pass: a small per-kill
624    /// chance alongside the boss and dungeon drops, so the collection has a
625    /// steady trickle instead of moving only on gate events. Scaled by the
626    /// dead mob's `wave_share` like every other per-kill payout.
627    #[schemars(
628        title = "Шанс камня артефакта с убийства моба",
629        description = "Доля в [0,1], домножается на wave_share убитого моба. Дополняет дроп с босса и подземелья, а не заменяет его."
630    )]
631    pub mob_kill_chance: f64,
632}
633
634/// All artifact tunables.
635#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Tsify)]
636#[tsify(from_wasm_abi, into_wasm_abi)]
637pub struct ArtifactsSettings {
638    /// Chapter at which the artifact navigation vertical becomes visible.
639    ///
640    /// UI only — it gates NOTHING about ownership. Since the acquisition pass
641    /// there is no per-template chapter gate at all: the starter is granted at
642    /// character creation, and every other artifact is a bundle reward (today,
643    /// the top-1 weekly rating bundles).
644    #[schemars(
645        title = "Глава открытия вертикали",
646        description = "Глава открытия навигации артефактов. Только UI: владение артефактами от неё не зависит."
647    )]
648    pub unlock_chapter: i64,
649
650    /// The guaranteed freebie. It exists so the stone system does not depend on
651    /// a rare drop, so it must always resolve to a real catalog entry —
652    /// `GameConfig::validate_artifacts` checks that.
653    #[schemars(
654        title = "Стартовый артефакт",
655        schema_with = "schema_loader::artifact_link_id_schema"
656    )]
657    pub starter_artifact_id: ArtifactTemplateId,
658
659    #[schemars(title = "Максимальный уровень артефакта")]
660    pub max_artifact_level: i64,
661
662    #[schemars(
663        title = "Лестница уровней артефакта: сырых копий за уровень",
664        description = "Явное улучшение тратит ровно стоимость следующего уровня. Получение дубликата только добавляет сырую копию, в том числе на максимальном уровне."
665    )]
666    pub upgrade_ladder: Vec<ArtifactUpgradeStep>,
667
668    #[schemars(title = "Расписание открытия сокетов")]
669    pub socket_unlocks: Vec<ArtifactSocketUnlock>,
670
671    #[schemars(title = "Максимальный уровень камня артефакта")]
672    pub max_stone_level: i64,
673
674    #[schemars(title = "Лестница прокачки камня артефакта: сырых копий за уровень")]
675    pub stone_upgrade_ladder: Vec<ArtifactUpgradeStep>,
676
677    /// `ART-03 Halfway Bell`'s only number, and deliberately a **share** rather
678    /// than an absolute threshold: `FlipSettings::progress_threshold` is a
679    /// balance knob that moves (the stones vertical rescales it), and a hard
680    /// number here would silently stop meaning "halfway" the moment it did.
681    #[schemars(
682        title = "Доля порога флипа под «Колоколом на полпути»",
683        description = "Доля обычного порога, на которой происходит Global Flip у носителя ART-03. 0.6 — «на 60% порога». Остальные источники Gauge не меняются."
684    )]
685    pub halfway_bell_threshold_share: f64,
686
687    /// `ART-02 Thin Mirror`'s only number: the share of Law Power a hidden law
688    /// woken by the World Law applies its effect at.
689    #[schemars(
690        title = "Доля силы скрытых законов под «Тонким зеркалом»",
691        description = "0.3 — скрытые законы применяют эффект на 30% силы. Скрытая активация не создаёт ни резонанс, ни заряд моста, ни шкалу флипа."
692    )]
693    pub thin_mirror_share: f64,
694
695    #[schemars(title = "ЗАГЛУШКА: источники камней артефакта")]
696    pub stone_drop: ArtifactStoneDropSettings,
697}
698
699impl ArtifactsSettings {
700    /// Whether `socket` is open at `chapter_level`. An unscheduled socket never
701    /// opens; `GameConfig::validate_artifacts` requires all six to be scheduled,
702    /// so this is a safety net rather than a reachable state.
703    pub fn is_socket_unlocked(&self, socket: ArtifactSocketSlot, chapter_level: i64) -> bool {
704        self.socket_unlocks
705            .iter()
706            .find(|unlock| unlock.socket == socket)
707            .is_some_and(|unlock| chapter_level >= unlock.unlock_chapter)
708    }
709
710    /// The share of the configured flip threshold `law` moves the bar to.
711    ///
712    /// `1.0` for every law but `ART-03`, so the caller never has to know which
713    /// law is which — and a nonsense configured share reads as "no change"
714    /// rather than as an instant or unreachable flip.
715    pub fn flip_threshold_share(&self, law: Option<ArtifactWorldLaw>) -> f64 {
716        match law {
717            Some(ArtifactWorldLaw::HalfwayBell) => {
718                let share = self.halfway_bell_threshold_share;
719                if share.is_finite() && share > 0.0 && share <= 1.0 {
720                    share
721                } else {
722                    1.0
723                }
724            }
725            Some(ArtifactWorldLaw::ThinMirror) | None => 1.0,
726        }
727    }
728
729    /// The share of Law Power hidden laws run at under `ART-02 Thin Mirror`, or
730    /// `0.0` for every other worn artifact (and for nothing worn).
731    ///
732    /// A nonsense configured share reads as "no wake" rather than as a free
733    /// full-strength second build — the conservative direction, since this rule
734    /// switches on half the build.
735    pub fn thin_mirror_share(&self, law: Option<ArtifactWorldLaw>) -> f64 {
736        match law {
737            Some(ArtifactWorldLaw::ThinMirror) => {
738                let share = self.thin_mirror_share;
739                if share.is_finite() && share > 0.0 && share <= 1.0 {
740                    share
741                } else {
742                    0.0
743                }
744            }
745            Some(ArtifactWorldLaw::HalfwayBell) | None => 0.0,
746        }
747    }
748
749    /// Raw copies needed to take an artifact **to** `target_level`. `None` means
750    /// the ladder has no such rung — at the cap, or a config gap that
751    /// validation rules out at load time.
752    pub fn upgrade_copies_required(&self, target_level: i64) -> Option<i64> {
753        self.upgrade_ladder
754            .iter()
755            .find(|step| step.level == target_level)
756            .map(|step| step.copies)
757    }
758
759    /// Same, for an artifact stone.
760    pub fn stone_upgrade_copies_required(&self, target_level: i64) -> Option<i64> {
761        self.stone_upgrade_ladder
762            .iter()
763            .find(|step| step.level == target_level)
764            .map(|step| step.copies)
765    }
766
767    /// The unlock chapter of the first socket of `column`, used by validation
768    /// and by tests to state the "left column first" rule without repeating the
769    /// slot list.
770    pub fn earliest_unlock_of(&self, column: ArtifactSocketColumn) -> Option<i64> {
771        self.socket_unlocks
772            .iter()
773            .filter(|unlock| unlock.socket.column() == column)
774            .map(|unlock| unlock.unlock_chapter)
775            .min()
776    }
777}
778
779#[cfg(test)]
780mod tests {
781    use super::*;
782
783    fn settings() -> ArtifactsSettings {
784        ArtifactsSettings {
785            unlock_chapter: 30,
786            starter_artifact_id: uuid::Uuid::from_u128(1),
787            max_artifact_level: 3,
788            upgrade_ladder: vec![
789                ArtifactUpgradeStep {
790                    level: 2,
791                    copies: 1,
792                },
793                ArtifactUpgradeStep {
794                    level: 3,
795                    copies: 2,
796                },
797            ],
798            socket_unlocks: vec![
799                ArtifactSocketUnlock {
800                    socket: ArtifactSocketSlot::VisibleAspect,
801                    unlock_chapter: 30,
802                },
803                ArtifactSocketUnlock {
804                    socket: ArtifactSocketSlot::HiddenAspect,
805                    unlock_chapter: 34,
806                },
807                ArtifactSocketUnlock {
808                    socket: ArtifactSocketSlot::FlipAspect,
809                    unlock_chapter: 38,
810                },
811                ArtifactSocketUnlock {
812                    socket: ArtifactSocketSlot::VisibleLaw,
813                    unlock_chapter: 42,
814                },
815                ArtifactSocketUnlock {
816                    socket: ArtifactSocketSlot::HiddenLaw,
817                    unlock_chapter: 46,
818                },
819                ArtifactSocketUnlock {
820                    socket: ArtifactSocketSlot::BridgeLaw,
821                    unlock_chapter: 50,
822                },
823            ],
824            max_stone_level: 5,
825            stone_upgrade_ladder: vec![ArtifactUpgradeStep {
826                level: 2,
827                copies: 2,
828            }],
829            halfway_bell_threshold_share: 0.6,
830            thin_mirror_share: 0.3,
831            stone_drop: ArtifactStoneDropSettings {
832                chapter_boss_chance: 0.1,
833                dungeon_chance: 0.2,
834                mob_kill_chance: 0.0,
835            },
836        }
837    }
838
839    /// The whole Aspect column opens before the first Law socket does, and every
840    /// Law socket now opens at a chapter a player can actually reach.
841    #[test]
842    fn the_aspect_column_opens_before_the_law_column() {
843        let settings = settings();
844        assert!(settings.is_socket_unlocked(ArtifactSocketSlot::VisibleAspect, 30));
845        assert!(!settings.is_socket_unlocked(ArtifactSocketSlot::FlipAspect, 30));
846        assert!(settings.is_socket_unlocked(ArtifactSocketSlot::FlipAspect, 38));
847
848        for law in [
849            ArtifactSocketSlot::VisibleLaw,
850            ArtifactSocketSlot::HiddenLaw,
851            ArtifactSocketSlot::BridgeLaw,
852        ] {
853            assert!(
854                !settings.is_socket_unlocked(law, 38),
855                "{law} must not open before the last Aspect socket"
856            );
857            assert!(
858                settings.is_socket_unlocked(law, 50),
859                "{law} must be reachable"
860            );
861        }
862        assert_eq!(
863            settings.earliest_unlock_of(ArtifactSocketColumn::Law),
864            Some(42)
865        );
866    }
867
868    #[test]
869    fn each_ladder_prices_its_own_levels() {
870        let settings = settings();
871        assert_eq!(settings.upgrade_copies_required(2), Some(1));
872        assert_eq!(settings.upgrade_copies_required(3), Some(2));
873        assert_eq!(settings.upgrade_copies_required(4), None);
874        assert_eq!(settings.stone_upgrade_copies_required(2), Some(2));
875    }
876
877    /// Acceptance criterion 9: a level moves the magnitude and nothing else.
878    #[test]
879    fn a_stone_level_scales_only_the_magnitude() {
880        let template = ArtifactStoneTemplate {
881            id: uuid::Uuid::from_u128(2),
882            name: i18n::I18nString::Translated(String::new()),
883            description: i18n::I18nString::Translated(String::new()),
884            socket: ArtifactSocketSlot::FlipAspect,
885            rule: ArtifactStoneRule::StaggeredEntrance,
886            magnitude: 70.0,
887            magnitude_per_level: 10.0,
888            secondary_magnitude: 0.0,
889            rule_param: 500,
890            tier_magnitudes: Vec::new(),
891            power_q_first_rank: 1.03,
892            power_q_max_rank: 1.06,
893            active: true,
894            icon_path: String::new(),
895        };
896
897        assert_eq!(template.magnitude_at_level(1), 70.0);
898        assert_eq!(template.magnitude_at_level(3), 90.0);
899        assert_eq!(template.magnitude_at_level(0), 70.0);
900        assert_eq!(
901            template.rule_param, 500,
902            "the rule's own parameter is level-independent"
903        );
904        assert_eq!(
905            template.secondary_magnitude, 0.0,
906            "and so is the second percentage"
907        );
908    }
909
910    /// `VA-05` carries its whole tier table in the catalog even while switched
911    /// off, and an unlisted tier reads as plain strength.
912    #[test]
913    fn the_tier_table_is_read_per_tier_and_defaults_to_plain() {
914        let template = ArtifactStoneTemplate {
915            id: uuid::Uuid::from_u128(3),
916            name: i18n::I18nString::Translated(String::new()),
917            description: i18n::I18nString::Translated(String::new()),
918            socket: ArtifactSocketSlot::VisibleAspect,
919            rule: ArtifactStoneRule::SmallGears,
920            magnitude: 130.0,
921            magnitude_per_level: 5.0,
922            secondary_magnitude: 0.0,
923            rule_param: 0,
924            tier_magnitudes: vec![
925                ArtifactTierMagnitude {
926                    tier: StoneTier::Common,
927                    magnitude: 130.0,
928                },
929                ArtifactTierMagnitude {
930                    tier: StoneTier::Rare,
931                    magnitude: 115.0,
932                },
933            ],
934            power_q_first_rank: 1.03,
935            power_q_max_rank: 1.06,
936            active: false,
937            icon_path: String::new(),
938        };
939
940        assert_eq!(template.tier_magnitude(StoneTier::Common), 130.0);
941        assert_eq!(template.tier_magnitude(StoneTier::Rare), 115.0);
942        assert_eq!(
943            template.tier_magnitude(StoneTier::Legendary),
944            100.0,
945            "an unlisted tier is plain strength, never silence"
946        );
947    }
948
949    #[test]
950    fn every_rule_declares_the_socket_it_belongs_to() {
951        use ArtifactStoneRule::*;
952        for rule in [
953            Crescendo,
954            Alternator,
955            FocusedEngine,
956            OpeningFive,
957            SmallGears,
958        ] {
959            assert_eq!(rule.socket(), ArtifactSocketSlot::VisibleAspect);
960        }
961        for rule in [
962            BackgroundVoice,
963            ShadowRound,
964            CrossRelay,
965            SecondHalf,
966            CatchUp,
967        ] {
968            assert_eq!(rule.socket(), ArtifactSocketSlot::HiddenAspect);
969        }
970        for rule in [
971            IncomingSalvo,
972            LastWord,
973            WarmStart,
974            Afterimage,
975            StaggeredEntrance,
976        ] {
977            assert_eq!(rule.socket(), ArtifactSocketSlot::FlipAspect);
978        }
979        for rule in [
980            SourceFocus,
981            CurrentPower,
982            ExportPower,
983            FullConfidence,
984            FirstArticle,
985        ] {
986            assert_eq!(rule.socket(), ArtifactSocketSlot::VisibleLaw);
987        }
988        for rule in [
989            ProxyReader,
990            LinkedEcho,
991            LateAwakening,
992            SingleLesson,
993            ReadyTarget,
994        ] {
995            assert_eq!(rule.socket(), ArtifactSocketSlot::HiddenLaw);
996        }
997        for rule in [ExtraSpan, DeepSpan, ShortSpan, ReciprocalGate] {
998            assert_eq!(rule.socket(), ArtifactSocketSlot::BridgeLaw);
999        }
1000    }
1001
1002    /// Nothing ships blocked any more: every left-column dependency landed with
1003    /// the stones v0.2 merge, and the right column's landed with the cores
1004    /// vertical. The guard itself stays for the next rule entered ahead of its
1005    /// mechanic.
1006    #[test]
1007    fn no_rule_declares_a_missing_dependency() {
1008        use ArtifactStoneRule::*;
1009        for rule in [
1010            Crescendo,
1011            Alternator,
1012            FocusedEngine,
1013            OpeningFive,
1014            SmallGears,
1015            BackgroundVoice,
1016            ShadowRound,
1017            CrossRelay,
1018            SecondHalf,
1019            CatchUp,
1020            IncomingSalvo,
1021            LastWord,
1022            WarmStart,
1023            Afterimage,
1024            StaggeredEntrance,
1025            SourceFocus,
1026            CurrentPower,
1027            ExportPower,
1028            FullConfidence,
1029            FirstArticle,
1030            ProxyReader,
1031            LinkedEcho,
1032            LateAwakening,
1033            SingleLesson,
1034            ReadyTarget,
1035            ExtraSpan,
1036            DeepSpan,
1037            ShortSpan,
1038            ReciprocalGate,
1039        ] {
1040            assert_eq!(rule.unavailable_dependency(), None, "{rule:?} runs today");
1041        }
1042    }
1043
1044    /// Which rules are worthless without a chosen law, and which wake a law of
1045    /// the side that is down. Both predicates gate real behaviour, so a rule
1046    /// added to the wrong list is a silently dead or a silently doubled stone.
1047    #[test]
1048    fn the_law_column_declares_which_rules_need_a_target_and_which_wake_hidden_laws() {
1049        use ArtifactStoneRule::*;
1050        for rule in [
1051            SourceFocus,
1052            CurrentPower,
1053            ExportPower,
1054            FullConfidence,
1055            ProxyReader,
1056            LinkedEcho,
1057            LateAwakening,
1058            SingleLesson,
1059            ReadyTarget,
1060            ReciprocalGate,
1061        ] {
1062            assert!(rule.needs_law_target(), "{rule:?} addresses one law");
1063        }
1064        // The build-wide rules: VL-05 changes the rhythm of a phase, BL-01..03
1065        // move every bridge.
1066        for rule in [FirstArticle, ExtraSpan, DeepSpan, ShortSpan, Crescendo] {
1067            assert!(!rule.needs_law_target(), "{rule:?} addresses no single law");
1068        }
1069
1070        for rule in [
1071            ProxyReader,
1072            LinkedEcho,
1073            LateAwakening,
1074            SingleLesson,
1075            ReadyTarget,
1076        ] {
1077            assert!(rule.wakes_hidden_law(), "{rule:?} is a Hidden Law rule");
1078        }
1079        for rule in [
1080            SourceFocus,
1081            CurrentPower,
1082            ExportPower,
1083            FullConfidence,
1084            FirstArticle,
1085            ExtraSpan,
1086            DeepSpan,
1087            ShortSpan,
1088            ReciprocalGate,
1089            BackgroundVoice,
1090        ] {
1091            assert!(
1092                !rule.wakes_hidden_law(),
1093                "{rule:?} must never produce a hidden law activation"
1094            );
1095        }
1096    }
1097
1098    /// `ART-03` moves the bar by a share of whatever threshold is configured;
1099    /// every other worn artifact leaves it alone, and a nonsense share is
1100    /// ignored rather than turned into an instant flip.
1101    #[test]
1102    fn the_halfway_bell_share_applies_only_to_its_own_law() {
1103        let mut settings = settings();
1104        assert_eq!(settings.flip_threshold_share(None), 1.0);
1105        assert_eq!(
1106            settings.flip_threshold_share(Some(ArtifactWorldLaw::ThinMirror)),
1107            1.0,
1108            "the other artifact does not touch the bar"
1109        );
1110        assert_eq!(
1111            settings.flip_threshold_share(Some(ArtifactWorldLaw::HalfwayBell)),
1112            0.6
1113        );
1114
1115        for broken in [0.0, -1.0, 1.5, f64::NAN] {
1116            settings.halfway_bell_threshold_share = broken;
1117            assert_eq!(
1118                settings.flip_threshold_share(Some(ArtifactWorldLaw::HalfwayBell)),
1119                1.0,
1120                "a broken share must read as 'no change'"
1121            );
1122        }
1123    }
1124
1125    /// `ART-02` wakes hidden laws only for its own wearer, and a nonsense share
1126    /// switches the rule OFF rather than granting a free second build.
1127    #[test]
1128    fn the_thin_mirror_share_applies_only_to_its_own_law() {
1129        let mut settings = settings();
1130        assert_eq!(settings.thin_mirror_share(None), 0.0);
1131        assert_eq!(
1132            settings.thin_mirror_share(Some(ArtifactWorldLaw::HalfwayBell)),
1133            0.0
1134        );
1135        assert_eq!(
1136            settings.thin_mirror_share(Some(ArtifactWorldLaw::ThinMirror)),
1137            0.3
1138        );
1139
1140        for broken in [0.0, -1.0, 1.5, f64::NAN] {
1141            settings.thin_mirror_share = broken;
1142            assert_eq!(
1143                settings.thin_mirror_share(Some(ArtifactWorldLaw::ThinMirror)),
1144                0.0,
1145                "a broken share must switch the rule off, never open it up"
1146            );
1147        }
1148    }
1149}