303 lines
9.4 KiB
C#
303 lines
9.4 KiB
C#
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();
|
||
}
|
||
}
|
||
}
|