Files
Cielonos/Assets/Scripts/MainGame/Interactions/MechanicalTable.cs
SoulliesOfficial 39b43680a9 爆更
2026-07-18 03:16:20 -04:00

303 lines
9.4 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
{
/// <summary>
/// Roguelite 宝箱房交互物。稀有度决定展示模型与 RewardProfile
/// 候选物品由 RewardGenerator 统一生成。
/// </summary>
public class MechanicalTable : InteractableObjectBase, ICombatRewardInteractable
{
[TitleGroup("刷新配置")]
[SerializeField, MinValue(0)]
private int maxRefreshCount = 1;
[ShowInInspector, ReadOnly]
private int _refreshesRemaining;
[TitleGroup("展示模型")]
[SerializeField]
private Dictionary<ItemRarity, GameObject> _tablePrefabs = new();
[SerializeField]
private GameObject _notAvailableTablePrefab;
[TitleGroup("Reward Profiles")]
[InfoBox("每个稀有度可使用不同 Profile同一个 Profile 可以被多个稀有度复用。")]
[SerializeField]
private Dictionary<ItemRarity, RewardProfile> _profiles = new();
[TitleGroup("运行时状态"), ReadOnly]
public ItemRarity rarity;
[ShowInInspector, ReadOnly]
private bool _isAvailable;
private Dictionary<ItemRarity, float> RarityWeights => MainGameManager.Config.mechanicalTableRarityWeights;
private List<ItemBase> _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));
}
/// <summary>
/// 由 RunManager 或场景加载流程预先配置。调用会丢弃尚未选择的旧候选。
/// </summary>
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<RareMaterial>(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();
}
}
}