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