using System; using System.Collections.Generic; using Cielonos.Core.Interaction; using Cielonos.MainGame.Inventory; using Cielonos.MainGame.Inventory.Collections; using Cielonos.MainGame.Rewards; using Cielonos.MainGame.UI; using Sirenix.OdinInspector; using UnityEngine; namespace Cielonos.MainGame.Interactions { /// /// Roguelite 宝箱房交互物。稀有度决定展示模型与 RewardProfile, /// 候选物品由 RewardGenerator 统一生成。 /// public class MechanicalTable : InteractableObjectBase, ICombatRewardInteractable { [TitleGroup("刷新配置")] [SerializeField, MinValue(0)] private int maxRefreshCount = 1; [ShowInInspector, ReadOnly] private int _refreshesRemaining; [TitleGroup("展示模型")] [SerializeField] private Dictionary _tablePrefabs = new(); [SerializeField] private GameObject _notAvailableTablePrefab; [TitleGroup("Reward Profiles")] [InfoBox("每个稀有度可使用不同 Profile;同一个 Profile 可以被多个稀有度复用。")] [SerializeField] private Dictionary _profiles = new(); [TitleGroup("运行时状态"), ReadOnly] public ItemRarity rarity; [ShowInInspector, ReadOnly] private bool _isAvailable; private Dictionary RarityWeights => MainGameManager.Config.mechanicalTableRarityWeights; private List _currentOffers = new(); private GameObject _currentTable; private Transform _offerStagingRoot; private void Start() { if (!_isAvailable) { Setup(new System.Random()); } } private void OnDestroy() { ClearCurrentOffers(); } protected override void InitializeChoices() { _refreshesRemaining = maxRefreshCount; choices.Add(new InteractionChoice("查看", OpenTable)); choices.Add(new InteractionChoice("刷新", RefreshTable, isInteractable: _refreshesRemaining > 0)); } /// /// 由 RunManager 或场景加载流程预先配置。调用会丢弃尚未选择的旧候选。 /// public void Setup(System.Random rng) { ClearCurrentOffers(); rarity = RollRarity(rng); if (_currentTable != null) { Destroy(_currentTable); } if (_tablePrefabs.TryGetValue(rarity, out GameObject tablePrefab) && tablePrefab != null) { _currentTable = Instantiate(tablePrefab, transform); } RewardProfile profile = GetProfile(); if (profile == null) { Debug.LogError($"[MechanicalTable] Rarity '{rarity}' has no RewardProfile assigned."); _isAvailable = false; RefreshChoiceStates(); return; } _currentOffers = RewardGenerator.GenerateRuntimeOffers(profile, rng, GetOfferStagingRoot(), RewardGenerationContext.FromPlayer(rarity).ExcludeOwnedNonConsumables()); _isAvailable = _currentOffers.Count > 0; if (!_isAvailable) { Debug.LogWarning($"[MechanicalTable] Profile '{profile.name}' generated no valid offers for rarity '{rarity}'."); } RefreshChoiceStates(); } public void SetupCombatReward(CombatRewardInstruction instruction, System.Random rng) { Setup(rng ?? new System.Random()); } private void OpenTable() { if (!_isAvailable) { System.Random rng = RunManager.Instance?.currentRun?.randomizer?.Next() ?? new System.Random(); Setup(rng); if (!_isAvailable) { return; } } var page = PlayerCanvas.MainGamePages.mechanicalTablePage; if (page == null) { Debug.LogError("[MechanicalTable] MechanicalTableUIPage is not registered."); return; } page.SetOffers(_currentOffers, HandleOfferSelected, HandleOfferSkipped); page.Open(); } private void RefreshTable() { if (_refreshesRemaining <= 0 || !_isAvailable) { return; } _refreshesRemaining--; System.Random rng = RunManager.Instance?.currentRun?.randomizer?.Next() ?? new System.Random(); Setup(rng); RefreshChoiceStates(); var interaction = MainGameManager.Player?.interactionSc; if (interaction != null) { PlayerCanvas.InteractionUIArea?.Show(interaction.currentChoices, interaction.currentChoiceIndex); } } private void HandleOfferSelected(int index) { if (index < 0 || index >= _currentOffers.Count) { return; } ItemBase selected = _currentOffers[index]; _currentOffers.RemoveAt(index); int amount = selected is ConsumableBase consumable ? consumable.stackAmount : 1; NotificationData notification = NotificationUIArea.CreateItemObtainedData(selected, amount); MainGameManager.Player.inventorySc.backpackSm.ObtainRewardOffer(selected); PlayerCanvas.NotificationUIArea?.Push(notification); ClearCurrentOffers(); CompleteMechanicalTable(); } private void HandleOfferSkipped() { RewardProfile profile = GetProfile(); System.Random rng = RunManager.Instance?.currentRun?.randomizer?.Next() ?? new System.Random(); int compensation = profile?.RollSkipCompensation(rng) ?? 0; if (compensation > 0) { MainGameManager.Player.inventorySc.backpackSm.ObtainItem(compensation); } PlayerCanvas.NotificationUIArea?.PushRareMaterial(compensation, "放弃机械台奖励"); ClearCurrentOffers(); CompleteMechanicalTable(); } private void CompleteMechanicalTable() { _isAvailable = false; RefreshChoiceStates(); if (_currentTable != null) { Destroy(_currentTable); } if (_notAvailableTablePrefab != null) { _currentTable = Instantiate(_notAvailableTablePrefab, transform); } SetExhausted(); if (RunManager.Instance == null || !RunManager.Instance.TryCompletePendingCombatReward()) { RunManager.Instance?.CompleteCurrentNode(); } } private RewardProfile GetProfile() { if (_profiles == null || _profiles.Count == 0) { return null; } if (_profiles.TryGetValue(rarity, out RewardProfile profile) && profile != null) { return profile; } foreach (RewardProfile fallback in _profiles.Values) { if (fallback == null) { continue; } Debug.LogWarning($"[MechanicalTable] Rarity '{rarity}' has no exact RewardProfile. Using fallback profile '{fallback.name}'."); return fallback; } return null; } private void RefreshChoiceStates() { if (choices == null || choices.Count == 0) { return; } choices[0].isInteractable = _isAvailable; if (choices.Count > 1) { choices[1].isInteractable = _refreshesRemaining > 0 && _isAvailable; } } private ItemRarity RollRarity(System.Random rng) { float totalWeight = 0f; foreach (var pair in RarityWeights) { totalWeight += pair.Value; } if (totalWeight <= 0f) { return ItemRarity.Tera; } float roll = (float)(rng.NextDouble() * totalWeight); float cumulative = 0f; foreach (var pair in RarityWeights) { cumulative += pair.Value; if (roll <= cumulative) { return pair.Key; } } return ItemRarity.Tera; } private Transform GetOfferStagingRoot() { if (_offerStagingRoot != null) { return _offerStagingRoot; } GameObject root = new("GeneratedOffers"); root.transform.SetParent(transform, false); root.SetActive(false); _offerStagingRoot = root.transform; return _offerStagingRoot; } private void ClearCurrentOffers() { foreach (ItemBase item in _currentOffers) { if (item != null) { Destroy(item.gameObject); } } _currentOffers.Clear(); } } }