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

285 lines
10 KiB
C#
Raw Permalink 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 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;
}
}
}