1use configs::game_config::GameConfig;
32use essences::flip::WorldSide;
33use essences::items::Item;
34use event_system::script::random::GameRng;
35use uuid::Uuid;
36
37use crate::behaviors::{BehaviorKind, BehaviorMeta, BehaviorRegistry};
38use crate::game_config_helpers::GameConfigLookup;
39use crate::mechanics::balance;
40use crate::mechanics::content_lookups::ContentLookups;
41
42pub struct ItemAttributeCtx<'a> {
47 pub item: &'a Item,
48 pub attributes_quantity: i64,
49 pub random: &'a GameRng,
50 pub config: &'a GameConfig,
51 pub lookups: &'a ContentLookups,
52}
53
54pub type ItemAttributeFn = fn(&ItemAttributeCtx) -> anyhow::Result<i64>;
57
58fn eff_item(ctx: &ItemAttributeCtx) -> f64 {
60 balance::eff_item_with_config(
61 ctx.config,
62 ctx.lookups,
63 ctx.item.item_template_id,
64 ctx.item.level as f64,
65 )
66}
67
68fn attr_spread(ctx: &ItemAttributeCtx) -> f64 {
70 balance::attr_spread_for_item(
71 ctx.config,
72 ctx.lookups,
73 ctx.random,
74 ctx.item.item_template_id,
75 ctx.item.level as f64,
76 )
77}
78
79fn aux_attr_eff(ctx: &ItemAttributeCtx, base_eff: f64) -> f64 {
81 balance::aux_attr_eff_for_item(
82 ctx.config,
83 ctx.lookups,
84 base_eff,
85 ctx.random,
86 ctx.item.item_template_id,
87 ctx.item.level as f64,
88 )
89}
90
91pub fn attr_health(ctx: &ItemAttributeCtx) -> anyhow::Result<i64> {
93 let base_attr_impact = 0.5;
105 let eff = eff_item(ctx);
106 let rand_mod = attr_spread(ctx);
107 let attr_eff = (eff * rand_mod).powf(base_attr_impact);
108 let hp_k = balance::hp_k_for_level(ctx.item.level as f64);
109 Ok((balance::BASE_HP * attr_eff * hp_k / 10.0).floor() as i64)
110}
111
112pub fn attr_armor(ctx: &ItemAttributeCtx) -> anyhow::Result<i64> {
114 let dmg_increase = balance::eff_spell_by_level(ctx.item.level as f64);
115 let spread = attr_spread(ctx);
116 let dr = (1.0 - 1.0 / (dmg_increase * spread)).max(0.03) * 10000.0;
117 Ok((dr / 10.0).floor() as i64)
118}
119
120pub fn attr_damage(ctx: &ItemAttributeCtx) -> anyhow::Result<i64> {
122 let base_attr_impact = 0.5;
134 let eff = eff_item(ctx);
135 let rand_mod = attr_spread(ctx);
136 let attr_eff = eff.powf(base_attr_impact) * rand_mod;
138 Ok((balance::BASE_ATTACK * attr_eff / 10.0).floor() as i64)
139}
140
141pub fn attr_crit_chance(ctx: &ItemAttributeCtx) -> anyhow::Result<i64> {
143 let eff = eff_item(ctx);
144 let attr_eff = aux_attr_eff(ctx, eff).powi(2);
145 let crit_chance = (-1.0 + (8.0 * attr_eff - 7.0).powf(0.5)) / 4.0;
146 Ok((crit_chance * 10000.0 / 10.0).floor() as i64)
147}
148
149pub fn attr_crit_damage(ctx: &ItemAttributeCtx) -> anyhow::Result<i64> {
151 let eff = eff_item(ctx);
152 let attr_eff = aux_attr_eff(ctx, eff).powi(2);
153 let crit_mod = (-1.0 + (8.0 * attr_eff - 7.0).powf(0.5)) / 2.0;
154 Ok((crit_mod * 10000.0 / 10.0).floor() as i64)
155}
156
157pub fn attr_evasion(ctx: &ItemAttributeCtx) -> anyhow::Result<i64> {
159 let eff = eff_item(ctx);
160 let attr_eff = aux_attr_eff(ctx, eff);
161 let evasion_chance = 1.0 - 1.0 / attr_eff;
162 Ok((evasion_chance * 10000.0 / 10.0).floor() as i64)
163}
164
165pub fn attr_speed(ctx: &ItemAttributeCtx) -> anyhow::Result<i64> {
167 let eff = eff_item(ctx);
168 let attr_eff = aux_attr_eff(ctx, eff);
169 Ok(((attr_eff - 1.0) * 10000.0 / 10.0).floor() as i64)
170}
171
172pub fn attr_hp_regen(ctx: &ItemAttributeCtx) -> anyhow::Result<i64> {
174 let eff = eff_item(ctx);
177 let base_attr_eff = eff;
178 let hp_eff = base_attr_eff.powf(0.5);
179 let hp = hp_eff * balance::BASE_HP;
180 let attr_eff = aux_attr_eff(ctx, eff);
181 let hp_per_sec = hp * (attr_eff - 1.0) / (balance::FIGHT_DURATION * attr_eff);
182 Ok((hp_per_sec / 10.0).floor() as i64)
183}
184
185pub fn attr_multi_cast(ctx: &ItemAttributeCtx) -> anyhow::Result<i64> {
187 let eff = eff_item(ctx);
188 let attr_eff = aux_attr_eff(ctx, eff);
189 Ok(((attr_eff - 1.0) * 10000.0 / 10.0).floor() as i64)
190}
191
192pub fn attr_counter_attack(ctx: &ItemAttributeCtx) -> anyhow::Result<i64> {
194 let eff = eff_item(ctx);
195 let attr_eff = aux_attr_eff(ctx, eff);
196 let dmg_increase = balance::eff_spell_by_level(ctx.item.level as f64);
197 let dps = dmg_increase * balance::SPELL_QUANTITY as f64;
198 let overall_damage = dps * balance::FIGHT_DURATION;
199 let added_damage = overall_damage * (attr_eff - 1.0);
200 let attacks_per_fight = balance::FIGHT_DURATION * balance::ATTACKS_PER_SEC;
201 let p = added_damage / attacks_per_fight / balance::COUNTERATTACK_POWER;
202 Ok((p * 10000.0 / 10.0).floor() as i64)
203}
204
205pub fn attr_bravery(ctx: &ItemAttributeCtx) -> anyhow::Result<i64> {
207 let eff = eff_item(ctx);
208 let attr_eff = aux_attr_eff(ctx, eff);
209 let p = balance::bravery_p_from_eff(attr_eff);
210 Ok((p * 10000.0 / 10.0).floor() as i64)
211}
212
213pub fn attr_guile(ctx: &ItemAttributeCtx) -> anyhow::Result<i64> {
215 let eff = eff_item(ctx);
216 let attr_eff = aux_attr_eff(ctx, eff);
217 let p = balance::deceit_p_from_eff(attr_eff);
218 Ok((p * 10000.0 / 10.0).floor() as i64)
219}
220
221pub fn attr_block(ctx: &ItemAttributeCtx) -> anyhow::Result<i64> {
223 let eff = eff_item(ctx);
224 let attr_eff = aux_attr_eff(ctx, eff).min(2.0);
226 let block_chance = 2.0 - 2.0 / attr_eff;
227 Ok((block_chance * 10000.0 / 10.0).floor() as i64)
228}
229
230pub fn attr_zero(_ctx: &ItemAttributeCtx) -> anyhow::Result<i64> {
233 Ok(0)
234}
235
236pub fn register(registry: &mut BehaviorRegistry) {
239 let fns: &[(&str, &str, &str, ItemAttributeFn)] = &[
240 (
241 "attr_health",
242 "Атрибут: здоровье",
243 "Порт calculation_behavior атрибута Health.",
244 attr_health,
245 ),
246 (
247 "attr_armor",
248 "Атрибут: броня",
249 "Порт calculation_behavior атрибута Armor.",
250 attr_armor,
251 ),
252 (
253 "attr_damage",
254 "Атрибут: урон",
255 "Порт calculation_behavior атрибута Damage.",
256 attr_damage,
257 ),
258 (
259 "attr_crit_chance",
260 "Атрибут: шанс крита",
261 "Порт calculation_behavior атрибута Crit_Chance.",
262 attr_crit_chance,
263 ),
264 (
265 "attr_crit_damage",
266 "Атрибут: крит. урон",
267 "Порт calculation_behavior атрибута Crit_Damage.",
268 attr_crit_damage,
269 ),
270 (
271 "attr_evasion",
272 "Атрибут: уклонение",
273 "Порт calculation_behavior атрибута Evasion.",
274 attr_evasion,
275 ),
276 (
277 "attr_speed",
278 "Атрибут: скорость",
279 "Порт calculation_behavior атрибута Speed.",
280 attr_speed,
281 ),
282 (
283 "attr_hp_regen",
284 "Атрибут: реген HP",
285 "Порт calculation_behavior атрибута HP_Regen.",
286 attr_hp_regen,
287 ),
288 (
289 "attr_multi_cast",
290 "Атрибут: мультикаст",
291 "Порт calculation_behavior атрибута Multi_Cast.",
292 attr_multi_cast,
293 ),
294 (
295 "attr_counter_attack",
296 "Атрибут: контратака",
297 "Порт calculation_behavior атрибута Counter_Attack.",
298 attr_counter_attack,
299 ),
300 (
301 "attr_bravery",
302 "Атрибут: храбрость",
303 "Порт calculation_behavior атрибута Bravery.",
304 attr_bravery,
305 ),
306 (
307 "attr_guile",
308 "Атрибут: коварство",
309 "Порт calculation_behavior атрибута Guile.",
310 attr_guile,
311 ),
312 (
313 "attr_block",
314 "Атрибут: блок",
315 "Порт calculation_behavior атрибута Block.",
316 attr_block,
317 ),
318 (
319 "attr_zero",
320 "Атрибут: ноль",
321 "Возвращает 0 (порт пустых / `0` calculation_behavior: \
322 Damage_Received, Bonus_Health, Bonus_damage).",
323 attr_zero,
324 ),
325 ];
326 for (name, title, description, f) in fns {
327 registry.register_item_attribute(
328 BehaviorMeta {
329 name: name.to_string(),
330 category: BehaviorKind::ItemAttribute,
331 title: title.to_string(),
332 description: description.to_string(),
333 },
334 *f,
335 );
336 }
337}
338
339const ITEMS_FOR_SLOTS: &[(&str, Option<WorldSide>, &str)] = &[
346 ("Boots", None, "0194d64e-216d-7059-863f-26f67c64267b"),
347 ("Ring", None, "0194d64e-216d-7059-863f-26fa1fc8410b"),
348 ("Waist", None, "0194d64e-216d-7059-863f-26fb000bf4e9"),
349 ("Legs", None, "0194d64e-216d-7059-863f-26fc9c33e67e"),
350 ("Neck", None, "0194d64e-216d-7059-863f-26fec3786536"),
351 (
352 "Torso",
353 Some(WorldSide::Fantasy),
354 "0194d64e-216d-7059-863f-26f7e69b8f4a",
355 ),
356 (
357 "Head",
358 Some(WorldSide::Fantasy),
359 "0194d64e-216d-7059-863f-26f85eb1e25f",
360 ),
361 (
362 "Gloves",
363 Some(WorldSide::Fantasy),
364 "0194d64e-216d-7059-863f-26f9ab123d46",
365 ),
366 (
367 "Shoulders",
368 Some(WorldSide::Fantasy),
369 "0194d64e-216d-7059-863f-26fddb278b04",
370 ),
371 (
372 "Weapon",
373 Some(WorldSide::Fantasy),
374 "0194d64e-216d-7059-863f-26ff77d309a3",
375 ),
376 (
377 "Torso",
378 Some(WorldSide::Real),
379 "019d2490-2623-74a4-a18f-0e045d50b127",
380 ), (
382 "Head",
383 Some(WorldSide::Real),
384 "019d2490-4df0-7ac4-a4ab-0714e7d873a3",
385 ), (
387 "Gloves",
388 Some(WorldSide::Real),
389 "019d2490-b494-74a5-97ab-ebfd9ab2fbaa",
390 ), (
392 "Shoulders",
393 Some(WorldSide::Real),
394 "019d2490-7fac-7a83-a1b0-0bef6a2ba599",
395 ), (
397 "Weapon",
398 Some(WorldSide::Real),
399 "019d2490-0262-7e9c-97e8-eea374634c5f",
400 ), ];
402
403pub struct ItemPriceCtx<'a> {
406 pub item: &'a Item,
407 pub config: &'a GameConfig,
408 pub lookups: &'a ContentLookups,
409}
410
411pub fn item_price(
420 ctx: &ItemPriceCtx,
421) -> anyhow::Result<Vec<event_system::script::types::ESCurrencyUnit>> {
422 let eff_level = crate::mechanics::balance::eff_by_level(ctx.item.level as f64);
432 let quality = ctx
433 .config
434 .item_template(ctx.item.item_template_id)
435 .and_then(|tpl| ctx.lookups.item_rarity_sell_q.get(&tpl.rarity_id).copied())
436 .unwrap_or(1.0);
437 let price = crate::mechanics::balance::sell_gold(
438 eff_level,
439 quality,
440 ctx.config.game_settings.sell_price_exp,
441 ctx.config.game_settings.sell_price_coef,
442 );
443 Ok(vec![event_system::script::types::ESCurrencyUnit {
444 currency_id: Uuid::from_u128(0x0194d64e_2386_7020_8b01_d6b3d5424506),
445 amount: price,
446 }])
447}
448
449pub struct ItemExperienceCtx<'a> {
451 pub item: &'a Item,
452 pub config: &'a GameConfig,
453 pub lookups: &'a ContentLookups,
454}
455
456pub fn item_experience_eff_item(ctx: &ItemExperienceCtx) -> anyhow::Result<i64> {
460 use crate::game_config_helpers::GameConfigLookup;
461
462 let quality = ctx
463 .config
464 .item_template(ctx.item.item_template_id)
465 .and_then(|template| ctx.lookups.item_rarity_q.get(&template.rarity_id).copied())
466 .unwrap_or(1.0);
467 Ok(crate::mechanics::balance::item_experience(
468 ctx.item.level as f64,
469 quality,
470 ))
471}
472
473pub struct ChestItemChooseCtx<'a> {
475 pub character: &'a essences::character_state::CharacterState,
476 pub config: &'a GameConfig,
477 pub lookups: &'a ContentLookups,
478 pub pending_items: &'a [Item],
483}
484
485pub fn chest_item_choose(ctx: &ChestItemChooseCtx) -> anyhow::Result<Option<Uuid>> {
489 use crate::mechanics::content;
490
491 let character = &ctx.character.character;
492
493 if let Some(&code) = character.custom_values.0.get("next_mimic_item_code")
495 && code != 0
496 && let Some(item) = content::get_item_by_code(ctx.config, ctx.lookups, code)
497 {
498 return Ok(Some(item.id));
499 }
500
501 let mut levels: Vec<&content::InventoryLevel> =
504 content::get_inventory_levels(ctx.config).iter().collect();
505 levels.sort_by_key(|l| std::cmp::Reverse(l.from_chapter_level));
506 let Some(current_level) = levels
507 .into_iter()
508 .find(|l| l.from_chapter_level <= character.current_chapter_level)
509 else {
510 return Ok(None);
511 };
512
513 let mut slots: Vec<_> = current_level
515 .slots
516 .iter()
517 .map(|slot| slot.equipment_slot_key())
518 .collect();
519
520 for item in ctx.character.inventory.iter().chain(ctx.pending_items) {
525 let filled = item.equipment_slot_key();
526 slots.retain(|slot| *slot != filled);
527 }
528
529 if let Some(selected_slot) = slots.first() {
530 let selected_item_type = selected_slot.item_type().to_string();
534 if let Some((_, _, uuid_str)) = ITEMS_FOR_SLOTS.iter().find(|(slot, side, _)| {
535 *slot == selected_item_type && *side == selected_slot.world_side()
536 }) {
537 let selected = Uuid::parse_str(uuid_str)
538 .map_err(|e| anyhow::anyhow!("ITEMS_FOR_SLOTS bad uuid {uuid_str:?}: {e}"))?;
539 if ctx
540 .config
541 .items
542 .iter()
543 .any(|item| item.id == selected && item.equipment_slot_key() == *selected_slot)
544 {
545 return Ok(Some(selected));
546 }
547 }
548 if let Some(item) = ctx
549 .config
550 .items
551 .iter()
552 .find(|item| item.equipment_slot_key() == *selected_slot)
553 {
554 return Ok(Some(item.id));
555 }
556 }
557
558 Ok(None)
559}
560
561#[cfg(test)]
562mod tests {
563 use super::*;
564 use essences::{
565 character_state::CharacterState,
566 item_case::InventorySlotConfig,
567 items::{Item, ItemType, WorldSide},
568 };
569
570 #[test]
571 fn items_for_slots_covers_both_sides_of_every_two_sided_type() {
572 for item_type in [
575 ItemType::Weapon,
576 ItemType::Torso,
577 ItemType::Head,
578 ItemType::Gloves,
579 ItemType::Shoulders,
580 ItemType::Boots,
581 ItemType::Legs,
582 ItemType::Neck,
583 ItemType::Ring,
584 ItemType::Waist,
585 ] {
586 let name = item_type.to_string();
587 let sides: Vec<Option<WorldSide>> = if item_type.supports_world_side() {
588 vec![Some(WorldSide::Fantasy), Some(WorldSide::Real)]
589 } else {
590 vec![None]
591 };
592 for side in sides {
593 assert!(
594 ITEMS_FOR_SLOTS
595 .iter()
596 .any(|(slot, s, _)| *slot == name && *s == side),
597 "ITEMS_FOR_SLOTS is missing a ({name}, {side:?}) starter entry"
598 );
599 }
600 }
601 }
602
603 #[test]
604 fn first_empty_slot_unlocks_real_side_at_flip_boundary() {
605 let mut config = configs::tests_game_config::generate_game_config_for_tests();
606 config.flip_settings.unlock_chapter = 3;
607 config.inventory_levels = vec![
608 essences::item_case::InventoryLevel {
609 from_chapter_level: 0,
610 slots: vec![InventorySlotConfig {
611 item_type: ItemType::Weapon,
612 world_side: Some(WorldSide::Fantasy),
613 }],
614 },
615 essences::item_case::InventoryLevel {
616 from_chapter_level: 3,
617 slots: vec![
618 InventorySlotConfig {
619 item_type: ItemType::Weapon,
620 world_side: Some(WorldSide::Fantasy),
621 },
622 InventorySlotConfig {
623 item_type: ItemType::Weapon,
624 world_side: Some(WorldSide::Real),
625 },
626 ],
627 },
628 ];
629 let fantasy_template = config
630 .items
631 .iter()
632 .find(|template| {
633 template.item_type == ItemType::Weapon
634 && template.world_side == Some(WorldSide::Fantasy)
635 })
636 .expect("test config must contain a Fantasy weapon");
637 let mut character = CharacterState::default();
638 character.character.current_chapter_level = 2;
639 character.inventory.push(Item {
640 id: Uuid::now_v7(),
641 item_template_id: fantasy_template.id,
642 item_type: fantasy_template.item_type,
643 world_side: fantasy_template.world_side,
644 is_equipped: true,
645 ..Default::default()
646 });
647
648 let selected_before_unlock = chest_item_choose(&ChestItemChooseCtx {
649 character: &character,
650 config: &config,
651 lookups: &ContentLookups::default(),
652 pending_items: &[],
653 })
654 .unwrap();
655 assert!(
656 selected_before_unlock.is_none(),
657 "the locked Real half must not count as an empty guaranteed slot"
658 );
659
660 character.character.current_chapter_level = 3;
661 let selected_id = chest_item_choose(&ChestItemChooseCtx {
662 character: &character,
663 config: &config,
664 lookups: &ContentLookups::default(),
665 pending_items: &[],
666 })
667 .unwrap()
668 .expect("the empty Real weapon slot must be selected");
669 let selected = config
670 .items
671 .iter()
672 .find(|template| template.id == selected_id)
673 .unwrap();
674
675 assert_eq!(selected.item_type, ItemType::Weapon);
676 assert_eq!(selected.world_side, Some(WorldSide::Real));
677 }
678
679 #[test]
680 fn first_empty_slot_respects_gloves_before_gated_legs() {
681 let mut config = configs::tests_game_config::generate_game_config_for_tests();
682 let mut fantasy_gloves = config
683 .items
684 .iter()
685 .find(|template| template.item_type == ItemType::Gloves)
686 .unwrap()
687 .clone();
688 fantasy_gloves.id = Uuid::now_v7();
689 fantasy_gloves.world_side = Some(WorldSide::Fantasy);
690 config.items.push(fantasy_gloves);
691 config.inventory_levels = vec![
692 essences::item_case::InventoryLevel {
693 from_chapter_level: 0,
694 slots: vec![InventorySlotConfig {
695 item_type: ItemType::Gloves,
696 world_side: Some(WorldSide::Fantasy),
697 }],
698 },
699 essences::item_case::InventoryLevel {
700 from_chapter_level: 6,
701 slots: vec![
702 InventorySlotConfig {
703 item_type: ItemType::Gloves,
704 world_side: Some(WorldSide::Fantasy),
705 },
706 InventorySlotConfig {
707 item_type: ItemType::Legs,
708 world_side: None,
709 },
710 ],
711 },
712 ];
713
714 let mut character = CharacterState::default();
715 let selected_id = chest_item_choose(&ChestItemChooseCtx {
716 character: &character,
717 config: &config,
718 lookups: &ContentLookups::default(),
719 pending_items: &[],
720 })
721 .unwrap()
722 .expect("the chapter-zero Gloves slot must be selected");
723 assert_eq!(
724 config
725 .items
726 .iter()
727 .find(|template| template.id == selected_id)
728 .unwrap()
729 .item_type,
730 ItemType::Gloves
731 );
732
733 character.inventory.extend(
734 config
735 .items
736 .iter()
737 .filter(|template| template.item_type == ItemType::Gloves)
738 .map(|template| Item {
739 id: Uuid::now_v7(),
740 item_template_id: template.id,
741 item_type: template.item_type,
742 world_side: template.world_side,
743 is_equipped: true,
744 ..Default::default()
745 }),
746 );
747
748 let selected_before_legs_unlock = chest_item_choose(&ChestItemChooseCtx {
749 character: &character,
750 config: &config,
751 lookups: &ContentLookups::default(),
752 pending_items: &[],
753 })
754 .unwrap();
755 assert!(
756 selected_before_legs_unlock.is_none(),
757 "the locked Legs slot must not receive a guaranteed chest item"
758 );
759
760 character.character.current_chapter_level = 6;
761 let selected_id = chest_item_choose(&ChestItemChooseCtx {
762 character: &character,
763 config: &config,
764 lookups: &ContentLookups::default(),
765 pending_items: &[],
766 })
767 .unwrap()
768 .expect("the Legs slot must become eligible at chapter six");
769 assert_eq!(
770 config
771 .items
772 .iter()
773 .find(|template| template.id == selected_id)
774 .unwrap()
775 .item_type,
776 ItemType::Legs
777 );
778 }
779
780 #[test]
784 fn first_empty_slot_counts_pending_and_bagged_items() {
785 let mut config = configs::tests_game_config::generate_game_config_for_tests();
786 config.inventory_levels = vec![essences::item_case::InventoryLevel {
787 from_chapter_level: 0,
788 slots: vec![
789 InventorySlotConfig {
790 item_type: ItemType::Weapon,
791 world_side: Some(WorldSide::Fantasy),
792 },
793 InventorySlotConfig {
794 item_type: ItemType::Weapon,
795 world_side: Some(WorldSide::Real),
796 },
797 ],
798 }];
799
800 let bagged_item = |config: &GameConfig, template_id: Uuid| {
801 let template = config
802 .items
803 .iter()
804 .find(|template| template.id == template_id)
805 .unwrap();
806 Item {
807 id: Uuid::now_v7(),
808 item_template_id: template.id,
809 item_type: template.item_type,
810 world_side: template.world_side,
811 is_equipped: false,
812 ..Default::default()
813 }
814 };
815
816 let mut character = CharacterState::default();
817 let first = chest_item_choose(&ChestItemChooseCtx {
818 character: &character,
819 config: &config,
820 lookups: &ContentLookups::default(),
821 pending_items: &[],
822 })
823 .unwrap()
824 .expect("the empty Fantasy weapon slot must be selected");
825
826 let pending = vec![bagged_item(&config, first)];
829 let second = chest_item_choose(&ChestItemChooseCtx {
830 character: &character,
831 config: &config,
832 lookups: &ContentLookups::default(),
833 pending_items: &pending,
834 })
835 .unwrap()
836 .expect("the second open of the batch must move on to the Real weapon slot");
837 assert_ne!(
838 first, second,
839 "a batch open must not repeat the same guaranteed item"
840 );
841
842 let pending = vec![bagged_item(&config, first), bagged_item(&config, second)];
843 assert!(
844 chest_item_choose(&ChestItemChooseCtx {
845 character: &character,
846 config: &config,
847 lookups: &ContentLookups::default(),
848 pending_items: &pending,
849 })
850 .unwrap()
851 .is_none(),
852 "once the batch covers every slot the rest must roll by weights"
853 );
854
855 character.inventory.push(bagged_item(&config, first));
857 let after_bagged = chest_item_choose(&ChestItemChooseCtx {
858 character: &character,
859 config: &config,
860 lookups: &ContentLookups::default(),
861 pending_items: &[],
862 })
863 .unwrap()
864 .expect("the still-empty Real weapon slot must be selected");
865 assert_eq!(
866 second, after_bagged,
867 "an unequipped item in the bag must count as filling its slot"
868 );
869 }
870}