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;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|