Files

285 lines
10 KiB
C#
Raw Permalink Normal View History

2026-07-18 03:16:20 -04:00
using System;
using System.Collections.Generic;
using System.Linq;
using Cielonos.MainGame.Inventory;
using Cielonos.MainGame.Inventory.Collections;
using Cielonos.MainGame.Items;
using UnityEngine;
namespace Cielonos.MainGame.Rewards
{
/// <summary>
/// RewardProfile 的运行时生成器。它产出 ItemBase
/// - prefab 候选适用于商店;
/// - 临时实例适用于确认后才能交给背包的奖励界面。
/// </summary>
public static class RewardGenerator
{
public static List<ItemBase> GeneratePrefabOffers(RewardProfile profile, System.Random rng,
RewardGenerationContext context = null)
{
return BuildOffers(profile, rng, context, null, false);
}
public static List<ItemBase> GenerateRuntimeOffers(RewardProfile profile, System.Random rng,
Transform stagingRoot, RewardGenerationContext context = null)
{
if (stagingRoot == null)
{
Debug.LogError("[RewardGenerator] Runtime offers require a staging root.");
return new List<ItemBase>();
}
return BuildOffers(profile, rng, context, stagingRoot, true);
}
private static List<ItemBase> BuildOffers(RewardProfile profile, System.Random rng,
RewardGenerationContext context, Transform stagingRoot, bool instantiate)
{
List<ItemBase> results = new();
if (profile == null || rng == null)
{
return results;
}
context ??= new RewardGenerationContext();
HashSet<RewardEntry> consumedEntries = new();
HashSet<ItemBase> selectedPrefabs = new();
List<RewardEntry> profileEntries = profile.entries ?? new List<RewardEntry>();
for (int slot = 0; slot < profile.offerCount; slot++)
{
List<RewardEntry> availableEntries = profileEntries
.Where(entry => IsEntryAvailable(entry, context, selectedPrefabs, consumedEntries, instantiate))
.ToList();
RewardEntry selectedEntry = PickEntry(availableEntries, rng);
if (selectedEntry == null)
{
break;
}
if (!TryResolvePrefab(selectedEntry, context, selectedPrefabs, rng, out ItemBase prefab,
out ItemBase upgradeTarget))
{
consumedEntries.Add(selectedEntry);
continue;
}
ItemBase result = prefab;
if (instantiate)
{
result = UnityEngine.Object.Instantiate(prefab, stagingRoot);
result.name = prefab.name;
ApplyRuntimeData(result, selectedEntry, upgradeTarget, rng);
}
else if (selectedEntry.mode == RewardEntryMode.UpgradeComponent)
{
Debug.LogWarning($"[RewardGenerator] Profile '{profile.name}' uses UpgradeComponent, which cannot be displayed as a prefab offer.");
consumedEntries.Add(selectedEntry);
continue;
}
results.Add(result);
selectedPrefabs.Add(prefab);
if (!selectedEntry.allowRepeat)
{
consumedEntries.Add(selectedEntry);
}
}
return results;
}
private static bool IsEntryAvailable(RewardEntry entry, RewardGenerationContext context,
ISet<ItemBase> selectedPrefabs, ISet<RewardEntry> consumedEntries, bool instantiate)
{
if (entry == null || entry.weight <= 0 || consumedEntries.Contains(entry))
{
return false;
}
return entry.mode switch
{
RewardEntryMode.FixedItem => CanSelectPrefab(entry.itemPrefab, context, selectedPrefabs,
entry.allowRepeat),
RewardEntryMode.ItemPool => entry.filter != null && ItemPool.HasCandidate(
BuildRuntimeFilter(entry, context), selectedPrefabs,
entry.filter.GetResourceFolders()),
RewardEntryMode.UpgradeComponent => instantiate && entry.itemPrefab != null &&
context.GetUpgradeableItems().Any(),
_ => false
};
}
private static RewardEntry PickEntry(List<RewardEntry> entries, System.Random rng)
{
int totalWeight = entries.Sum(entry => entry.weight);
if (totalWeight <= 0)
{
return null;
}
int roll = rng.Next(totalWeight);
int cumulative = 0;
foreach (RewardEntry entry in entries)
{
cumulative += entry.weight;
if (roll < cumulative)
{
return entry;
}
}
return entries[^1];
}
private static bool TryResolvePrefab(RewardEntry entry, RewardGenerationContext context,
ISet<ItemBase> selectedPrefabs, System.Random rng, out ItemBase prefab, out ItemBase upgradeTarget)
{
prefab = null;
upgradeTarget = null;
switch (entry.mode)
{
case RewardEntryMode.FixedItem:
prefab = entry.itemPrefab;
return prefab != null;
case RewardEntryMode.ItemPool:
{
List<ItemBase> rolled = ItemPool.Roll(1, rng,
BuildRuntimeFilter(entry, context), selectedPrefabs,
entry.filter.GetResourceFolders());
prefab = rolled.Count > 0 ? rolled[0] : null;
return prefab != null;
}
case RewardEntryMode.UpgradeComponent:
{
List<ItemBase> targets = context.GetUpgradeableItems();
if (targets.Count == 0)
{
return false;
}
prefab = entry.itemPrefab;
upgradeTarget = targets[rng.Next(targets.Count)];
return prefab != null;
}
default:
return false;
}
}
private static bool CanSelectPrefab(ItemBase prefab, RewardGenerationContext context,
ISet<ItemBase> selectedPrefabs, bool allowRepeat)
{
if (prefab == null || context.ShouldExcludeOwnedNonConsumable(prefab))
{
return false;
}
if (selectedPrefabs == null || !selectedPrefabs.Contains(prefab))
{
return true;
}
// Consumable 可以按配置重复;主武器、支援装备、被动装备必须保持唯一性。
return allowRepeat && RewardGenerationContext.IsConsumable(prefab);
}
private static ItemFilter BuildRuntimeFilter(RewardEntry entry, RewardGenerationContext context)
{
ItemFilter filter = entry.filter.BuildRuntimeFilter(context.forcedRarity);
if (!context.excludeOwnedNonConsumables)
{
return filter;
}
return filter.And(new ItemFilter(item => !context.ShouldExcludeOwnedNonConsumable(item)));
}
private static void ApplyRuntimeData(ItemBase item, RewardEntry entry, ItemBase upgradeTarget,
System.Random rng)
{
if (item is ConsumableBase consumable)
{
int min = Mathf.Max(1, Mathf.Min(entry.quantityRange.x, entry.quantityRange.y));
int max = Mathf.Max(min, entry.quantityRange.x, entry.quantityRange.y);
consumable.stackAmount = rng.Next(min, max + 1);
}
if (entry.mode == RewardEntryMode.UpgradeComponent && item is PrefabricatedComponent component)
{
component.targetEquipment = upgradeTarget;
}
}
}
public class RewardGenerationContext
{
public ItemRarity forcedRarity = ItemRarity.None;
public List<ItemBase> ownedItems = new();
public bool excludeOwnedNonConsumables;
/// <summary>
/// 开启后,奖励生成会排除玩家已经拥有的主武器、支援装备和被动装备。
/// Consumable 不参与该排除规则,仍然可以重复出现并堆叠。
/// </summary>
public RewardGenerationContext ExcludeOwnedNonConsumables()
{
excludeOwnedNonConsumables = true;
return this;
}
public bool ShouldExcludeOwnedNonConsumable(ItemBase item)
{
if (!excludeOwnedNonConsumables || item == null || IsConsumable(item))
{
return false;
}
Type itemIdentity = GetItemIdentity(item);
return itemIdentity != null && ownedItems.Any(owned => GetItemIdentity(owned) == itemIdentity);
}
public List<ItemBase> GetUpgradeableItems()
{
return ownedItems.Where(item => item != null && item.IsUpgradable).ToList();
}
public static bool IsConsumable(ItemBase item)
{
return item is ConsumableBase;
}
private static Type GetItemIdentity(ItemBase item)
{
if (item == null)
{
return null;
}
return item.contentData != null && item.contentData.itemClass != null
? item.contentData.itemClass
: item.GetType();
}
public static RewardGenerationContext FromPlayer(ItemRarity forcedRarity = ItemRarity.None)
{
RewardGenerationContext context = new() { forcedRarity = forcedRarity };
var backpack = MainGameManager.Player?.inventorySc?.backpackSm;
if (backpack == null)
{
return context;
}
context.ownedItems.AddRange(backpack.mainWeapons);
context.ownedItems.AddRange(backpack.supportEquipments);
context.ownedItems.AddRange(backpack.passiveEquipments);
return context;
}
}
}