Files
Cielonos/Assets/Scripts/MainGame/Interactions/MechanicalTable.cs

303 lines
9.4 KiB
C#
Raw Normal View History

2026-05-10 11:47:55 -04:00
using System;
using System.Collections.Generic;
using Cielonos.Core.Interaction;
2026-05-23 08:27:50 -04:00
using Cielonos.MainGame.Inventory;
2026-07-18 03:16:20 -04:00
using Cielonos.MainGame.Inventory.Collections;
using Cielonos.MainGame.Rewards;
2026-05-10 11:47:55 -04:00
using Cielonos.MainGame.UI;
using Sirenix.OdinInspector;
using UnityEngine;
namespace Cielonos.MainGame.Interactions
{
/// <summary>
2026-07-18 03:16:20 -04:00
/// Roguelite 宝箱房交互物。稀有度决定展示模型与 RewardProfile
/// 候选物品由 RewardGenerator 统一生成。
2026-05-10 11:47:55 -04:00
/// </summary>
2026-07-18 03:16:20 -04:00
public class MechanicalTable : InteractableObjectBase, ICombatRewardInteractable
2026-05-10 11:47:55 -04:00
{
2026-06-12 17:11:39 -04:00
[TitleGroup("刷新配置")]
2026-07-18 03:16:20 -04:00
[SerializeField, MinValue(0)]
2026-06-12 17:11:39 -04:00
private int maxRefreshCount = 1;
2026-07-18 03:16:20 -04:00
2026-06-12 17:11:39 -04:00
[ShowInInspector, ReadOnly]
private int _refreshesRemaining;
2026-07-18 03:16:20 -04:00
[TitleGroup("展示模型")]
2026-05-10 11:47:55 -04:00
[SerializeField]
2026-07-18 03:16:20 -04:00
private Dictionary<ItemRarity, GameObject> _tablePrefabs = new();
2026-05-10 11:47:55 -04:00
[SerializeField]
private GameObject _notAvailableTablePrefab;
2026-07-18 03:16:20 -04:00
[TitleGroup("Reward Profiles")]
[InfoBox("每个稀有度可使用不同 Profile同一个 Profile 可以被多个稀有度复用。")]
[SerializeField]
private Dictionary<ItemRarity, RewardProfile> _profiles = new();
2026-05-10 11:47:55 -04:00
[TitleGroup("运行时状态"), ReadOnly]
public ItemRarity rarity;
2026-07-18 03:16:20 -04:00
2026-06-27 12:52:03 -04:00
[ShowInInspector, ReadOnly]
2026-05-10 11:47:55 -04:00
private bool _isAvailable;
2026-07-18 03:16:20 -04:00
private Dictionary<ItemRarity, float> RarityWeights => MainGameManager.Config.mechanicalTableRarityWeights;
private List<ItemBase> _currentOffers = new();
private GameObject _currentTable;
private Transform _offerStagingRoot;
2026-05-10 11:47:55 -04:00
private void Start()
{
if (!_isAvailable)
{
Setup(new System.Random());
}
}
2026-07-18 03:16:20 -04:00
private void OnDestroy()
{
ClearCurrentOffers();
}
2026-05-10 11:47:55 -04:00
protected override void InitializeChoices()
{
2026-06-12 17:11:39 -04:00
_refreshesRemaining = maxRefreshCount;
choices.Add(new InteractionChoice("查看", OpenTable));
choices.Add(new InteractionChoice("刷新", RefreshTable, isInteractable: _refreshesRemaining > 0));
2026-05-10 11:47:55 -04:00
}
/// <summary>
2026-07-18 03:16:20 -04:00
/// 由 RunManager 或场景加载流程预先配置。调用会丢弃尚未选择的旧候选。
2026-05-10 11:47:55 -04:00
/// </summary>
public void Setup(System.Random rng)
{
2026-07-18 03:16:20 -04:00
ClearCurrentOffers();
2026-05-10 11:47:55 -04:00
rarity = RollRarity(rng);
2026-07-18 03:16:20 -04:00
if (_currentTable != null)
{
Destroy(_currentTable);
}
2026-05-10 11:47:55 -04:00
if (_tablePrefabs.TryGetValue(rarity, out GameObject tablePrefab) && tablePrefab != null)
{
_currentTable = Instantiate(tablePrefab, transform);
}
2026-07-18 03:16:20 -04:00
RewardProfile profile = GetProfile();
if (profile == null)
2026-05-10 11:47:55 -04:00
{
2026-07-18 03:16:20 -04:00
Debug.LogError($"[MechanicalTable] Rarity '{rarity}' has no RewardProfile assigned.");
_isAvailable = false;
RefreshChoiceStates();
return;
2026-05-10 11:47:55 -04:00
}
2026-07-18 03:16:20 -04:00
_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());
2026-05-10 11:47:55 -04:00
}
private void OpenTable()
{
2026-06-12 17:11:39 -04:00
if (!_isAvailable)
2026-05-10 11:47:55 -04:00
{
2026-07-18 03:16:20 -04:00
System.Random rng = RunManager.Instance?.currentRun?.randomizer?.Next() ?? new System.Random();
Setup(rng);
if (!_isAvailable)
{
return;
}
2026-05-10 11:47:55 -04:00
}
2026-07-18 03:16:20 -04:00
var page = PlayerCanvas.MainGamePages.mechanicalTablePage;
if (page == null)
2026-05-10 11:47:55 -04:00
{
2026-07-18 03:16:20 -04:00
Debug.LogError("[MechanicalTable] MechanicalTableUIPage is not registered.");
2026-05-10 11:47:55 -04:00
return;
}
2026-07-18 03:16:20 -04:00
page.SetOffers(_currentOffers, HandleOfferSelected, HandleOfferSkipped);
page.Open();
2026-05-10 11:47:55 -04:00
}
2026-06-12 17:11:39 -04:00
private void RefreshTable()
{
2026-07-18 03:16:20 -04:00
if (_refreshesRemaining <= 0 || !_isAvailable)
{
return;
}
2026-06-12 17:11:39 -04:00
_refreshesRemaining--;
System.Random rng = RunManager.Instance?.currentRun?.randomizer?.Next() ?? new System.Random();
Setup(rng);
2026-07-18 03:16:20 -04:00
RefreshChoiceStates();
var interaction = MainGameManager.Player?.interactionSc;
if (interaction != null)
{
PlayerCanvas.InteractionUIArea?.Show(interaction.currentChoices, interaction.currentChoiceIndex);
}
}
2026-06-12 17:11:39 -04:00
2026-07-18 03:16:20 -04:00
private void HandleOfferSelected(int index)
{
if (index < 0 || index >= _currentOffers.Count)
2026-06-12 17:11:39 -04:00
{
2026-07-18 03:16:20 -04:00
return;
2026-06-12 17:11:39 -04:00
}
2026-07-18 03:16:20 -04:00
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();
2026-06-12 17:11:39 -04:00
}
2026-07-18 03:16:20 -04:00
private void HandleOfferSkipped()
2026-05-10 11:47:55 -04:00
{
2026-07-18 03:16:20 -04:00
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);
}
2026-05-10 11:47:55 -04:00
2026-07-18 03:16:20 -04:00
PlayerCanvas.NotificationUIArea?.PushRareMaterial(compensation, "放弃机械台奖励");
2026-05-10 11:47:55 -04:00
2026-07-18 03:16:20 -04:00
ClearCurrentOffers();
CompleteMechanicalTable();
}
2026-05-10 11:47:55 -04:00
2026-07-18 03:16:20 -04:00
private void CompleteMechanicalTable()
{
2026-05-10 11:47:55 -04:00
_isAvailable = false;
2026-07-18 03:16:20 -04:00
RefreshChoiceStates();
if (_currentTable != null)
{
Destroy(_currentTable);
}
if (_notAvailableTablePrefab != null)
{
_currentTable = Instantiate(_notAvailableTablePrefab, transform);
}
2026-05-10 11:47:55 -04:00
2026-06-27 12:52:03 -04:00
SetExhausted();
2026-07-18 03:16:20 -04:00
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;
}
2026-05-10 11:47:55 -04:00
2026-07-18 03:16:20 -04:00
return null;
2026-05-10 11:47:55 -04:00
}
2026-07-18 03:16:20 -04:00
private void RefreshChoiceStates()
{
if (choices == null || choices.Count == 0)
{
return;
}
choices[0].isInteractable = _isAvailable;
if (choices.Count > 1)
{
choices[1].isInteractable = _refreshesRemaining > 0 && _isAvailable;
}
}
2026-05-10 11:47:55 -04:00
private ItemRarity RollRarity(System.Random rng)
{
float totalWeight = 0f;
2026-07-18 03:16:20 -04:00
foreach (var pair in RarityWeights)
2026-05-10 11:47:55 -04:00
{
2026-07-18 03:16:20 -04:00
totalWeight += pair.Value;
2026-05-10 11:47:55 -04:00
}
if (totalWeight <= 0f)
{
return ItemRarity.Tera;
}
float roll = (float)(rng.NextDouble() * totalWeight);
float cumulative = 0f;
2026-07-18 03:16:20 -04:00
foreach (var pair in RarityWeights)
2026-05-10 11:47:55 -04:00
{
2026-07-18 03:16:20 -04:00
cumulative += pair.Value;
2026-05-10 11:47:55 -04:00
if (roll <= cumulative)
{
2026-07-18 03:16:20 -04:00
return pair.Key;
2026-05-10 11:47:55 -04:00
}
}
return ItemRarity.Tera;
}
2026-07-18 03:16:20 -04:00
private Transform GetOfferStagingRoot()
2026-05-10 11:47:55 -04:00
{
2026-07-18 03:16:20 -04:00
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);
}
}
2026-05-10 11:47:55 -04:00
2026-07-18 03:16:20 -04:00
_currentOffers.Clear();
2026-05-10 11:47:55 -04:00
}
}
}