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, i18n::Translate)]
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    #[translate]
429    pub name: i18n::I18nString,
430
431    #[schemars(title = "Описание")]
432    #[translate]
433    pub description: i18n::I18nString,
434
435    /// The welded-in rule, or `None` for an artifact that is a bare frame.
436    #[schemars(
437        title = "World Law",
438        description = "Правило, вшитое в артефакт. Работает только пока артефакт надет. Пусто — артефакт без правила (стартовая «Пустая оправа»)."
439    )]
440    pub world_law: Option<ArtifactWorldLaw>,
441
442    /// What owning this artifact pays, per attribute. Empty = pays nothing.
443    ///
444    /// Fully per-template: any set of attributes, any values. Every owned
445    /// artifact pays — worn or not — which is the one thing that makes a second
446    /// artifact worth having once only one can be worn.
447    #[schemars(
448        title = "Бонусы за владение",
449        description = "Список атрибутов и процентов, которые артефакт даёт за владение. Пусто — артефакт не даёт статов."
450    )]
451    pub ownership_bonuses: Vec<ArtifactOwnershipBonusStat>,
452
453    #[schemars(
454        title = "Иконка",
455        schema_with = "schema_loader::asset_artifact_icon_schema"
456    )]
457    pub icon_path: String,
458
459    #[schemars(
460        title = "Множитель Power World Law",
461        description = "Приблизительный вклад World Law этого артефакта в displayed Power, пока артефакт надет (BAL-030). От ранга не зависит; 1.0 у артефакта без правила."
462    )]
463    pub power_q: f64,
464}
465
466/// One entry of the artifact-stone catalog.
467///
468/// The first copy carries the whole rule; `magnitude_per_level` is the only
469/// thing an upgrade moves (design §3), which is why the rule and `rule_param`
470/// are level-independent.
471#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Tsify, i18n::Translate)]
472#[tsify(from_wasm_abi, into_wasm_abi)]
473pub struct ArtifactStoneTemplate {
474    #[schemars(
475        title = "Множитель Power на первом ранге",
476        description = "Приблизительный вклад в displayed/matchmaking Power (BAL-030). 1.0 = не влияет. Промежуточные ранги интерполируются линейно по ln(q)."
477    )]
478    pub power_q_first_rank: f64,
479
480    #[schemars(
481        title = "Множитель Power на максимальном ранге",
482        description = "Значение того же множителя на последнем ранге. Должен быть не меньше значения на первом."
483    )]
484    pub power_q_max_rank: f64,
485
486    #[schemars(schema_with = "schema_loader::id_schema")]
487    pub id: ArtifactStoneTemplateId,
488
489    #[schemars(title = "Название")]
490    #[translate]
491    pub name: i18n::I18nString,
492
493    #[schemars(title = "Описание")]
494    #[translate]
495    pub description: i18n::I18nString,
496
497    #[schemars(title = "Сокет")]
498    pub socket: ArtifactSocketSlot,
499
500    #[schemars(title = "Правило")]
501    pub rule: ArtifactStoneRule,
502
503    #[schemars(
504        title = "Базовая величина, %",
505        description = "Величина правила на 1-м уровне, в процентах. Правило целиком даётся первой копией; уровень двигает только это число."
506    )]
507    pub magnitude: f64,
508
509    #[schemars(title = "Прибавка величины за уровень прокачки, %")]
510    pub magnitude_per_level: f64,
511
512    #[schemars(
513        title = "Вторая величина правила, %",
514        description = "Второй процент правила: нижняя точка разгона у «Крещендо», сила повтора у «Чередования», потолок у «Сфокусированного мотора», доля шкалы у «Второй половины». Остальным правилам не нужна (0). Прокачкой не меняется."
515    )]
516    pub secondary_magnitude: f64,
517
518    #[schemars(
519        title = "Параметр правила",
520        description = "Период в тиках для «Теневого круга» и «Поочерёдного входа», число срабатываний для «Послеобраза»; для остальных правил не используется (0). Прокачкой не меняется."
521    )]
522    pub rule_param: i64,
523
524    #[schemars(
525        title = "Сила по тирам триггера",
526        description = "Только для «Мелких шестерён» (VA-05). У остальных камней список пустой."
527    )]
528    pub tier_magnitudes: Vec<ArtifactTierMagnitude>,
529
530    #[schemars(
531        title = "Правило включено",
532        description = "Выключенный камень остаётся в каталоге со своими числами, но рантайм его не исполняет. Так заведены правила, которым нужна ещё не отгруженная механика."
533    )]
534    pub active: bool,
535
536    #[schemars(
537        title = "Иконка",
538        schema_with = "schema_loader::asset_artifact_stone_icon_schema"
539    )]
540    pub icon_path: String,
541}
542
543impl ArtifactStoneTemplate {
544    /// The rule's magnitude on a stone of upgrade level `level`, in percent.
545    /// Same shape as `configs::stones::StoneStat::value_at_level`, so both
546    /// systems scale with a level identically.
547    pub fn magnitude_at_level(&self, level: i64) -> f64 {
548        let levels_above_first = (level - 1).max(0) as f64;
549        self.magnitude + self.magnitude_per_level * levels_above_first
550    }
551
552    /// `VA-05 Small Gears` only: the share effects run at when the trigger that
553    /// fired them had `tier`. An unlisted tier reads as plain strength rather
554    /// than as zero, so a half-filled table can never silence a build.
555    pub fn tier_magnitude(&self, tier: StoneTier) -> f64 {
556        self.tier_magnitudes
557            .iter()
558            .find(|entry| entry.tier == tier)
559            .map_or(100.0, |entry| entry.magnitude)
560    }
561}
562
563/// Ownership stat bonus of ONE artifact at a given level, in percent.
564///
565/// One rung of an upgrade ladder: how many raw copies buy `level`.
566///
567/// Used by both ladders — artifacts and artifact stones. In both cases a
568/// duplicate banks a raw copy and an explicit player action spends the rung.
569#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, Tsify)]
570#[tsify(from_wasm_abi, into_wasm_abi)]
571pub struct ArtifactUpgradeStep {
572    #[schemars(
573        title = "Уровень, который покупается",
574        description = "Лестница начинается со 2-го уровня и должна доходить до максимального без пропусков."
575    )]
576    pub level: i64,
577
578    #[schemars(
579        title = "Сколько сырых копий нужно",
580        description = "Сам предмет в это число не входит. Копии считаются штуками."
581    )]
582    pub copies: i64,
583}
584
585/// When one artifact socket opens.
586///
587/// The order is fixed by design — the whole Aspect column before the Law column
588/// — and `GameConfig::validate_artifacts` enforces it.
589///
590/// The three Law sockets used to be scheduled past the end of the campaign,
591/// because their stones had no laws to retune. The cores vertical shipped, so
592/// they carry real chapters now; the shipped schedule continues the left
593/// column's four-chapter cadence (see `artifacts_settings/_data.yaml` for the
594/// numbers and the reasoning).
595#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, Tsify)]
596#[tsify(from_wasm_abi, into_wasm_abi)]
597pub struct ArtifactSocketUnlock {
598    #[schemars(title = "Сокет")]
599    pub socket: ArtifactSocketSlot,
600
601    #[schemars(
602        title = "Глава разблокировки",
603        description = "BAL-007: подписанные главы 91/111/131/151/171/191. Порядок фиксирован: сначала вся левая колонка (Aspect), потом правая (Law)."
604    )]
605    pub unlock_chapter: i64,
606}
607
608/// BAL-031 source: where artifact stones drop from (signed rates).
609///
610/// Deliberately a different faucet from the artifacts themselves (design §5), so
611/// the two collections never compete for the same drop.
612#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, Tsify)]
613#[tsify(from_wasm_abi, into_wasm_abi)]
614pub struct ArtifactStoneDropSettings {
615    #[schemars(
616        title = "Шанс камня артефакта с босса главы",
617        description = "Доля в [0,1]. BAL-031: подписанное значение 0.02."
618    )]
619    pub chapter_boss_chance: f64,
620
621    #[schemars(
622        title = "Шанс камня артефакта за пройденное подземелье",
623        description = "Доля в [0,1]. BAL-031: подписанное значение 0.15."
624    )]
625    pub dungeon_chance: f64,
626
627    /// The third faucet, added by the acquisition pass: a small per-kill
628    /// chance alongside the boss and dungeon drops, so the collection has a
629    /// steady trickle instead of moving only on gate events. Scaled by the
630    /// dead mob's `wave_share` like every other per-kill payout.
631    #[schemars(
632        title = "Шанс камня артефакта с убийства моба",
633        description = "Доля в [0,1], домножается на wave_share убитого моба. Дополняет дроп с босса и подземелья, а не заменяет его."
634    )]
635    pub mob_kill_chance: f64,
636}
637
638/// All artifact tunables.
639#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Tsify)]
640#[tsify(from_wasm_abi, into_wasm_abi)]
641pub struct ArtifactsSettings {
642    /// Chapter at which the artifact navigation vertical becomes visible.
643    ///
644    /// UI only — it gates NOTHING about ownership. Since the acquisition pass
645    /// there is no per-template chapter gate at all: the starter is granted at
646    /// character creation, and every other artifact is a bundle reward (today,
647    /// the top-1 weekly rating bundles).
648    #[schemars(
649        title = "Глава открытия вертикали",
650        description = "Глава открытия навигации артефактов. Только UI: владение артефактами от неё не зависит."
651    )]
652    pub unlock_chapter: i64,
653
654    /// The guaranteed freebie. It exists so the stone system does not depend on
655    /// a rare drop, so it must always resolve to a real catalog entry —
656    /// `GameConfig::validate_artifacts` checks that.
657    #[schemars(
658        title = "Стартовый артефакт",
659        schema_with = "schema_loader::artifact_link_id_schema"
660    )]
661    pub starter_artifact_id: ArtifactTemplateId,
662
663    #[schemars(title = "Максимальный уровень артефакта")]
664    pub max_artifact_level: i64,
665
666    #[schemars(
667        title = "Лестница уровней артефакта: сырых копий за уровень",
668        description = "Явное улучшение тратит ровно стоимость следующего уровня. Получение дубликата только добавляет сырую копию, в том числе на максимальном уровне."
669    )]
670    pub upgrade_ladder: Vec<ArtifactUpgradeStep>,
671
672    #[schemars(title = "Расписание открытия сокетов")]
673    pub socket_unlocks: Vec<ArtifactSocketUnlock>,
674
675    #[schemars(title = "Максимальный уровень камня артефакта")]
676    pub max_stone_level: i64,
677
678    #[schemars(title = "Лестница прокачки камня артефакта: сырых копий за уровень")]
679    pub stone_upgrade_ladder: Vec<ArtifactUpgradeStep>,
680
681    /// `ART-03 Halfway Bell`'s only number, and deliberately a **share** rather
682    /// than an absolute threshold: `FlipSettings::progress_threshold` is a
683    /// balance knob that moves (the stones vertical rescales it), and a hard
684    /// number here would silently stop meaning "halfway" the moment it did.
685    #[schemars(
686        title = "Доля порога флипа под «Колоколом на полпути»",
687        description = "Доля обычного порога, на которой происходит Global Flip у носителя ART-03. 0.6 — «на 60% порога». Остальные источники Gauge не меняются."
688    )]
689    pub halfway_bell_threshold_share: f64,
690
691    /// `ART-02 Thin Mirror`'s only number: the share of Law Power a hidden law
692    /// woken by the World Law applies its effect at.
693    #[schemars(
694        title = "Доля силы скрытых законов под «Тонким зеркалом»",
695        description = "0.3 — скрытые законы применяют эффект на 30% силы. Скрытая активация не создаёт ни резонанс, ни заряд моста, ни шкалу флипа."
696    )]
697    pub thin_mirror_share: f64,
698
699    #[schemars(title = "ЗАГЛУШКА: источники камней артефакта")]
700    pub stone_drop: ArtifactStoneDropSettings,
701}
702
703impl ArtifactsSettings {
704    /// Whether `socket` is open at `chapter_level`. An unscheduled socket never
705    /// opens; `GameConfig::validate_artifacts` requires all six to be scheduled,
706    /// so this is a safety net rather than a reachable state.
707    pub fn is_socket_unlocked(&self, socket: ArtifactSocketSlot, chapter_level: i64) -> bool {
708        self.socket_unlocks
709            .iter()
710            .find(|unlock| unlock.socket == socket)
711            .is_some_and(|unlock| chapter_level >= unlock.unlock_chapter)
712    }
713
714    /// The share of the configured flip threshold `law` moves the bar to.
715    ///
716    /// `1.0` for every law but `ART-03`, so the caller never has to know which
717    /// law is which — and a nonsense configured share reads as "no change"
718    /// rather than as an instant or unreachable flip.
719    pub fn flip_threshold_share(&self, law: Option<ArtifactWorldLaw>) -> f64 {
720        match law {
721            Some(ArtifactWorldLaw::HalfwayBell) => {
722                let share = self.halfway_bell_threshold_share;
723                if share.is_finite() && share > 0.0 && share <= 1.0 {
724                    share
725                } else {
726                    1.0
727                }
728            }
729            Some(ArtifactWorldLaw::ThinMirror) | None => 1.0,
730        }
731    }
732
733    /// The share of Law Power hidden laws run at under `ART-02 Thin Mirror`, or
734    /// `0.0` for every other worn artifact (and for nothing worn).
735    ///
736    /// A nonsense configured share reads as "no wake" rather than as a free
737    /// full-strength second build — the conservative direction, since this rule
738    /// switches on half the build.
739    pub fn thin_mirror_share(&self, law: Option<ArtifactWorldLaw>) -> f64 {
740        match law {
741            Some(ArtifactWorldLaw::ThinMirror) => {
742                let share = self.thin_mirror_share;
743                if share.is_finite() && share > 0.0 && share <= 1.0 {
744                    share
745                } else {
746                    0.0
747                }
748            }
749            Some(ArtifactWorldLaw::HalfwayBell) | None => 0.0,
750        }
751    }
752
753    /// Raw copies needed to take an artifact **to** `target_level`. `None` means
754    /// the ladder has no such rung — at the cap, or a config gap that
755    /// validation rules out at load time.
756    pub fn upgrade_copies_required(&self, target_level: i64) -> Option<i64> {
757        self.upgrade_ladder
758            .iter()
759            .find(|step| step.level == target_level)
760            .map(|step| step.copies)
761    }
762
763    /// Same, for an artifact stone.
764    pub fn stone_upgrade_copies_required(&self, target_level: i64) -> Option<i64> {
765        self.stone_upgrade_ladder
766            .iter()
767            .find(|step| step.level == target_level)
768            .map(|step| step.copies)
769    }
770
771    /// The unlock chapter of the first socket of `column`, used by validation
772    /// and by tests to state the "left column first" rule without repeating the
773    /// slot list.
774    pub fn earliest_unlock_of(&self, column: ArtifactSocketColumn) -> Option<i64> {
775        self.socket_unlocks
776            .iter()
777            .filter(|unlock| unlock.socket.column() == column)
778            .map(|unlock| unlock.unlock_chapter)
779            .min()
780    }
781}
782
783#[cfg(test)]
784mod tests {
785    use super::*;
786
787    fn settings() -> ArtifactsSettings {
788        ArtifactsSettings {
789            unlock_chapter: 30,
790            starter_artifact_id: uuid::Uuid::from_u128(1),
791            max_artifact_level: 3,
792            upgrade_ladder: vec![
793                ArtifactUpgradeStep {
794                    level: 2,
795                    copies: 1,
796                },
797                ArtifactUpgradeStep {
798                    level: 3,
799                    copies: 2,
800                },
801            ],
802            socket_unlocks: vec![
803                ArtifactSocketUnlock {
804                    socket: ArtifactSocketSlot::VisibleAspect,
805                    unlock_chapter: 30,
806                },
807                ArtifactSocketUnlock {
808                    socket: ArtifactSocketSlot::HiddenAspect,
809                    unlock_chapter: 34,
810                },
811                ArtifactSocketUnlock {
812                    socket: ArtifactSocketSlot::FlipAspect,
813                    unlock_chapter: 38,
814                },
815                ArtifactSocketUnlock {
816                    socket: ArtifactSocketSlot::VisibleLaw,
817                    unlock_chapter: 42,
818                },
819                ArtifactSocketUnlock {
820                    socket: ArtifactSocketSlot::HiddenLaw,
821                    unlock_chapter: 46,
822                },
823                ArtifactSocketUnlock {
824                    socket: ArtifactSocketSlot::BridgeLaw,
825                    unlock_chapter: 50,
826                },
827            ],
828            max_stone_level: 5,
829            stone_upgrade_ladder: vec![ArtifactUpgradeStep {
830                level: 2,
831                copies: 2,
832            }],
833            halfway_bell_threshold_share: 0.6,
834            thin_mirror_share: 0.3,
835            stone_drop: ArtifactStoneDropSettings {
836                chapter_boss_chance: 0.1,
837                dungeon_chance: 0.2,
838                mob_kill_chance: 0.0,
839            },
840        }
841    }
842
843    /// The whole Aspect column opens before the first Law socket does, and every
844    /// Law socket now opens at a chapter a player can actually reach.
845    #[test]
846    fn the_aspect_column_opens_before_the_law_column() {
847        let settings = settings();
848        assert!(settings.is_socket_unlocked(ArtifactSocketSlot::VisibleAspect, 30));
849        assert!(!settings.is_socket_unlocked(ArtifactSocketSlot::FlipAspect, 30));
850        assert!(settings.is_socket_unlocked(ArtifactSocketSlot::FlipAspect, 38));
851
852        for law in [
853            ArtifactSocketSlot::VisibleLaw,
854            ArtifactSocketSlot::HiddenLaw,
855            ArtifactSocketSlot::BridgeLaw,
856        ] {
857            assert!(
858                !settings.is_socket_unlocked(law, 38),
859                "{law} must not open before the last Aspect socket"
860            );
861            assert!(
862                settings.is_socket_unlocked(law, 50),
863                "{law} must be reachable"
864            );
865        }
866        assert_eq!(
867            settings.earliest_unlock_of(ArtifactSocketColumn::Law),
868            Some(42)
869        );
870    }
871
872    #[test]
873    fn each_ladder_prices_its_own_levels() {
874        let settings = settings();
875        assert_eq!(settings.upgrade_copies_required(2), Some(1));
876        assert_eq!(settings.upgrade_copies_required(3), Some(2));
877        assert_eq!(settings.upgrade_copies_required(4), None);
878        assert_eq!(settings.stone_upgrade_copies_required(2), Some(2));
879    }
880
881    /// Acceptance criterion 9: a level moves the magnitude and nothing else.
882    #[test]
883    fn a_stone_level_scales_only_the_magnitude() {
884        let template = ArtifactStoneTemplate {
885            id: uuid::Uuid::from_u128(2),
886            name: i18n::I18nString::Translated(String::new()),
887            description: i18n::I18nString::Translated(String::new()),
888            socket: ArtifactSocketSlot::FlipAspect,
889            rule: ArtifactStoneRule::StaggeredEntrance,
890            magnitude: 70.0,
891            magnitude_per_level: 10.0,
892            secondary_magnitude: 0.0,
893            rule_param: 500,
894            tier_magnitudes: Vec::new(),
895            power_q_first_rank: 1.03,
896            power_q_max_rank: 1.06,
897            active: true,
898            icon_path: String::new(),
899        };
900
901        assert_eq!(template.magnitude_at_level(1), 70.0);
902        assert_eq!(template.magnitude_at_level(3), 90.0);
903        assert_eq!(template.magnitude_at_level(0), 70.0);
904        assert_eq!(
905            template.rule_param, 500,
906            "the rule's own parameter is level-independent"
907        );
908        assert_eq!(
909            template.secondary_magnitude, 0.0,
910            "and so is the second percentage"
911        );
912    }
913
914    /// `VA-05` carries its whole tier table in the catalog even while switched
915    /// off, and an unlisted tier reads as plain strength.
916    #[test]
917    fn the_tier_table_is_read_per_tier_and_defaults_to_plain() {
918        let template = ArtifactStoneTemplate {
919            id: uuid::Uuid::from_u128(3),
920            name: i18n::I18nString::Translated(String::new()),
921            description: i18n::I18nString::Translated(String::new()),
922            socket: ArtifactSocketSlot::VisibleAspect,
923            rule: ArtifactStoneRule::SmallGears,
924            magnitude: 130.0,
925            magnitude_per_level: 5.0,
926            secondary_magnitude: 0.0,
927            rule_param: 0,
928            tier_magnitudes: vec![
929                ArtifactTierMagnitude {
930                    tier: StoneTier::Common,
931                    magnitude: 130.0,
932                },
933                ArtifactTierMagnitude {
934                    tier: StoneTier::Rare,
935                    magnitude: 115.0,
936                },
937            ],
938            power_q_first_rank: 1.03,
939            power_q_max_rank: 1.06,
940            active: false,
941            icon_path: String::new(),
942        };
943
944        assert_eq!(template.tier_magnitude(StoneTier::Common), 130.0);
945        assert_eq!(template.tier_magnitude(StoneTier::Rare), 115.0);
946        assert_eq!(
947            template.tier_magnitude(StoneTier::Legendary),
948            100.0,
949            "an unlisted tier is plain strength, never silence"
950        );
951    }
952
953    #[test]
954    fn every_rule_declares_the_socket_it_belongs_to() {
955        use ArtifactStoneRule::*;
956        for rule in [
957            Crescendo,
958            Alternator,
959            FocusedEngine,
960            OpeningFive,
961            SmallGears,
962        ] {
963            assert_eq!(rule.socket(), ArtifactSocketSlot::VisibleAspect);
964        }
965        for rule in [
966            BackgroundVoice,
967            ShadowRound,
968            CrossRelay,
969            SecondHalf,
970            CatchUp,
971        ] {
972            assert_eq!(rule.socket(), ArtifactSocketSlot::HiddenAspect);
973        }
974        for rule in [
975            IncomingSalvo,
976            LastWord,
977            WarmStart,
978            Afterimage,
979            StaggeredEntrance,
980        ] {
981            assert_eq!(rule.socket(), ArtifactSocketSlot::FlipAspect);
982        }
983        for rule in [
984            SourceFocus,
985            CurrentPower,
986            ExportPower,
987            FullConfidence,
988            FirstArticle,
989        ] {
990            assert_eq!(rule.socket(), ArtifactSocketSlot::VisibleLaw);
991        }
992        for rule in [
993            ProxyReader,
994            LinkedEcho,
995            LateAwakening,
996            SingleLesson,
997            ReadyTarget,
998        ] {
999            assert_eq!(rule.socket(), ArtifactSocketSlot::HiddenLaw);
1000        }
1001        for rule in [ExtraSpan, DeepSpan, ShortSpan, ReciprocalGate] {
1002            assert_eq!(rule.socket(), ArtifactSocketSlot::BridgeLaw);
1003        }
1004    }
1005
1006    /// Nothing ships blocked any more: every left-column dependency landed with
1007    /// the stones v0.2 merge, and the right column's landed with the cores
1008    /// vertical. The guard itself stays for the next rule entered ahead of its
1009    /// mechanic.
1010    #[test]
1011    fn no_rule_declares_a_missing_dependency() {
1012        use ArtifactStoneRule::*;
1013        for rule in [
1014            Crescendo,
1015            Alternator,
1016            FocusedEngine,
1017            OpeningFive,
1018            SmallGears,
1019            BackgroundVoice,
1020            ShadowRound,
1021            CrossRelay,
1022            SecondHalf,
1023            CatchUp,
1024            IncomingSalvo,
1025            LastWord,
1026            WarmStart,
1027            Afterimage,
1028            StaggeredEntrance,
1029            SourceFocus,
1030            CurrentPower,
1031            ExportPower,
1032            FullConfidence,
1033            FirstArticle,
1034            ProxyReader,
1035            LinkedEcho,
1036            LateAwakening,
1037            SingleLesson,
1038            ReadyTarget,
1039            ExtraSpan,
1040            DeepSpan,
1041            ShortSpan,
1042            ReciprocalGate,
1043        ] {
1044            assert_eq!(rule.unavailable_dependency(), None, "{rule:?} runs today");
1045        }
1046    }
1047
1048    /// Which rules are worthless without a chosen law, and which wake a law of
1049    /// the side that is down. Both predicates gate real behaviour, so a rule
1050    /// added to the wrong list is a silently dead or a silently doubled stone.
1051    #[test]
1052    fn the_law_column_declares_which_rules_need_a_target_and_which_wake_hidden_laws() {
1053        use ArtifactStoneRule::*;
1054        for rule in [
1055            SourceFocus,
1056            CurrentPower,
1057            ExportPower,
1058            FullConfidence,
1059            ProxyReader,
1060            LinkedEcho,
1061            LateAwakening,
1062            SingleLesson,
1063            ReadyTarget,
1064            ReciprocalGate,
1065        ] {
1066            assert!(rule.needs_law_target(), "{rule:?} addresses one law");
1067        }
1068        // The build-wide rules: VL-05 changes the rhythm of a phase, BL-01..03
1069        // move every bridge.
1070        for rule in [FirstArticle, ExtraSpan, DeepSpan, ShortSpan, Crescendo] {
1071            assert!(!rule.needs_law_target(), "{rule:?} addresses no single law");
1072        }
1073
1074        for rule in [
1075            ProxyReader,
1076            LinkedEcho,
1077            LateAwakening,
1078            SingleLesson,
1079            ReadyTarget,
1080        ] {
1081            assert!(rule.wakes_hidden_law(), "{rule:?} is a Hidden Law rule");
1082        }
1083        for rule in [
1084            SourceFocus,
1085            CurrentPower,
1086            ExportPower,
1087            FullConfidence,
1088            FirstArticle,
1089            ExtraSpan,
1090            DeepSpan,
1091            ShortSpan,
1092            ReciprocalGate,
1093            BackgroundVoice,
1094        ] {
1095            assert!(
1096                !rule.wakes_hidden_law(),
1097                "{rule:?} must never produce a hidden law activation"
1098            );
1099        }
1100    }
1101
1102    /// `ART-03` moves the bar by a share of whatever threshold is configured;
1103    /// every other worn artifact leaves it alone, and a nonsense share is
1104    /// ignored rather than turned into an instant flip.
1105    #[test]
1106    fn the_halfway_bell_share_applies_only_to_its_own_law() {
1107        let mut settings = settings();
1108        assert_eq!(settings.flip_threshold_share(None), 1.0);
1109        assert_eq!(
1110            settings.flip_threshold_share(Some(ArtifactWorldLaw::ThinMirror)),
1111            1.0,
1112            "the other artifact does not touch the bar"
1113        );
1114        assert_eq!(
1115            settings.flip_threshold_share(Some(ArtifactWorldLaw::HalfwayBell)),
1116            0.6
1117        );
1118
1119        for broken in [0.0, -1.0, 1.5, f64::NAN] {
1120            settings.halfway_bell_threshold_share = broken;
1121            assert_eq!(
1122                settings.flip_threshold_share(Some(ArtifactWorldLaw::HalfwayBell)),
1123                1.0,
1124                "a broken share must read as 'no change'"
1125            );
1126        }
1127    }
1128
1129    /// `ART-02` wakes hidden laws only for its own wearer, and a nonsense share
1130    /// switches the rule OFF rather than granting a free second build.
1131    #[test]
1132    fn the_thin_mirror_share_applies_only_to_its_own_law() {
1133        let mut settings = settings();
1134        assert_eq!(settings.thin_mirror_share(None), 0.0);
1135        assert_eq!(
1136            settings.thin_mirror_share(Some(ArtifactWorldLaw::HalfwayBell)),
1137            0.0
1138        );
1139        assert_eq!(
1140            settings.thin_mirror_share(Some(ArtifactWorldLaw::ThinMirror)),
1141            0.3
1142        );
1143
1144        for broken in [0.0, -1.0, 1.5, f64::NAN] {
1145            settings.thin_mirror_share = broken;
1146            assert_eq!(
1147                settings.thin_mirror_share(Some(ArtifactWorldLaw::ThinMirror)),
1148                0.0,
1149                "a broken share must switch the rule off, never open it up"
1150            );
1151        }
1152    }
1153}