Files
5.24BuYu/Assets/MyGame/aa/UI/Home/Sub/Activity/ChouJiangPanel.cs
T
2026-10-07 23:05:51 +08:00

488 lines
19 KiB
C#
Raw 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 Assets.MyGame.Scripts.Net;
using Assets.MyGame.Scripts.Net.API.GameConfig_SubClass;
using Assets.MyGame.Scripts.Net.APIRequest.Params;
using Assets.MyGame.Scripts.Net.APIResponses;
using System;
using System.Collections;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
/// <summary>奖池分组(swagger:0=旧调用兼容,1=炮台,2=战神,3=翅膀)</summary>
public enum LotteryPoolType
{
旧调用 = 0,
炮台 = 1,
战神 = 2,
翅膀 = 3,
}
/// <summary>
/// 抽奖活动总面板(第一层·最外层,ChouJiang.prefab 根节点)。
///
/// 三层结构:
/// 第一层 本面板:拉配置、切分组、抽奖、花费/次数显示;
/// 第二层 ChouJiangToggle:挂 PT炮台/CB翅膀/ZS战神 三个 toggle 面板,一个 toggle 对应一个面板,
/// 面板里一堆 item 分为 A/B/C 三个列表(items_A/items_B/items_C);
/// 第三层 ChouJiangItem:挂每个奖品格子,记录道具信息以及奖励的详细信息。
///
/// 流程:OnShow 拉取活动配置(/api/ActivityConfig/lotteryConfig)→ 初始化各 toggle 的 item
/// → 拉取已抽奖项置灰 + 用户状态(/api/Lottery/UserStatus)显示花费/次数
/// → 点抽奖调 /api/Lottery/Draw?poolType=当前分组。
///
/// 花费规则(swagger):costs 独立计费非累加——第 N+1 次抽花 costs[N],超出长度取最后一个;
/// userMaxDraw=0 不限次数;isFinished 或达到上限后不能再抽。
/// </summary>
public class ChouJiangPanel : SubUIBase
{
[Tooltip("三个 toggle 面板按顺序拖:[0]=炮台,[1]=战神,[2]=翅膀(poolType 按列表顺序自动赋值)")]
public List<ChouJiangToggle> toggles;
[Tooltip("分组切换按钮按顺序拖:[0]=炮台,[1]=战神,[2]=翅膀(还没做就留空)")]
public List<Toggle> toggles_PoolType;
[Header("可选 UI(没摆就留空,不影响逻辑)")]
public Button btn_Draw; // 抽奖按钮
public TextMeshProUGUI tmp_ActivityName; // 活动名
public TextMeshProUGUI tmp_Cost; // 下一次抽奖花费
public TextMeshProUGUI tmp_DrawCount; // 已抽次数/上限
public TextMeshProUGUI tmp_EndTime; // 活动结束时间/状态
[Tooltip("抽奖花费的货币类型:1=金币,3=人民币(¥),其他=钻石")]
public int costPayType = 1;
/// <summary>当前展示/抽奖的奖池分组</summary>
public LotteryPoolType currPoolType { get; private set; }
private LotteryActivityConfig config; // 活动配置
private LotteryUserStatus userStatus; // 用户抽奖状态(次数决定花费与上限)
/// <summary>已抽中的奖项ID(兜底重抽 + 跑马灯只跳未抽的格子)</summary>
private readonly HashSet<string> drawnIds = new HashSet<string>();
private bool isDrawing; // 防连点
public override void Awake()
{
base.Awake();
// toggles 列表顺序即分组:[0]=炮台,[1]=战神,[2]=翅膀(按顺序自动赋 poolType,不用逐个手设)
if (toggles != null)
{
int n = Mathf.Min(toggles.Count, 3);
for (int i = 0; i < n; i++)
{
if (toggles[i] != null)
toggles[i].poolType = (LotteryPoolType)(i + 1);
}
}
// 分组切换:一个按钮对应一个 toggle 面板(索引 0/1/2 = 炮台/战神/翅膀)
if (toggles_PoolType != null)
{
for (int i = 0; i < toggles_PoolType.Count; i++)
{
if (toggles_PoolType[i] == null) continue;
LotteryPoolType type = (LotteryPoolType)(i + 1);
Toggle toggle = toggles_PoolType[i];
toggle.onValueChanged.AddListener(isOn =>
{
if (!isOn) return;
if (isDrawing)
{
// 抽奖过程中禁止切换:勾选拔回当前分组,保持按钮与面板一致
toggle.SetIsOnWithoutNotify(false);
int curIdx = (int)currPoolType - 1;
if (curIdx >= 0 && curIdx < toggles_PoolType.Count && toggles_PoolType[curIdx] != null)
toggles_PoolType[curIdx].SetIsOnWithoutNotify(true);
return;
}
ShowPoolType(type);
});
}
}
if (btn_Draw != null)
btn_Draw.onClick.AddListener(OnDrawClick);
// 初始分组 = 预制体里当前激活的面板,并同步一次面板/按钮状态
ChouJiangToggle first = GetActiveToggle();
currPoolType = first != null ? first.poolType : LotteryPoolType.炮台;
ShowPoolType(currPoolType);
}
protected override void OnShow(object args = null)
{
base.OnShow(args);
Refresh();
}
/// <summary>拉取抽奖配置 + 刷新各 toggle 与用户状态。
/// 用 /api/Lottery/Config(新版统一接口:config + drawnPrizeIds 一次返回),
/// 不再用 /api/ActivityConfig/lotteryConfig + DrawnPrizes 两个旧请求。</summary>
public void Refresh()
{
APIRequestList.GetLotteryConfig(res =>
{
if (this == null) return;
config = res != null ? res.config : null;
// 活动未开启:提示后关闭面板
if (config != null && !config.isOpen)
{
UIManager.Instance.ShowTiShi("活动未开启");
uIManager.Hide(this);
return;
}
UpdateActivityUI();
InitToggles();
// 本期已获得奖品 → 置灰(以服务端返回为准)
ApplyDrawnIds(res != null ? res.drawnPrizeIds : null);
RefreshUserStatus();
});
}
/// <summary>初始化各 toggle 面板的 item(A/B/C 三个列表)</summary>
private void InitToggles()
{
if (toggles == null || config == null) return;
for (int i = 0; i < toggles.Count; i++)
{
if (toggles[i] != null)
toggles[i].Init(config);
}
}
/// <summary>拉取用户抽奖状态(各池已抽次数决定下一次花费与是否到上限),并刷新置灰</summary>
private void RefreshUserStatus()
{
APIRequestList.GetLotteryUserStatus(status =>
{
if (this == null) return;
userStatus = status;
// UserStatus 里同样带本期 drawnPrizeIds,以服务端最新数据刷新置灰
ApplyDrawnIds(status != null ? status.drawnPrizeIds : null);
UpdateDrawUI();
});
}
/// <summary>以服务端返回的已获得奖品ID列表为准刷新 drawnIds 并置灰各 toggle</summary>
private void ApplyDrawnIds(List<string> ids)
{
drawnIds.Clear();
if (ids != null)
{
for (int i = 0; i < ids.Count; i++)
drawnIds.Add(ids[i]);
}
if (toggles == null) return;
for (int i = 0; i < toggles.Count; i++)
{
if (toggles[i] != null)
toggles[i].RefreshDrawnState(ids);
}
}
/// <summary>切到指定分组的 toggle 面板(抽奖过程中禁止切换:跑马灯期间切池会看不到结果)</summary>
public void ShowPoolType(LotteryPoolType type)
{
if (isDrawing) return;
currPoolType = type;
if (toggles != null)
{
for (int i = 0; i < toggles.Count; i++)
{
if (toggles[i] == null) continue;
toggles[i].gameObject.SetActive(toggles[i].poolType == type);
}
}
// 同步切换按钮勾选状态(不触发回调)
if (toggles_PoolType != null)
{
int idx = (int)type - 1;
for (int i = 0; i < toggles_PoolType.Count; i++)
{
if (toggles_PoolType[i] == null || i == idx) continue;
toggles_PoolType[i].SetIsOnWithoutNotify(false);
}
if (idx >= 0 && idx < toggles_PoolType.Count && toggles_PoolType[idx] != null)
toggles_PoolType[idx].SetIsOnWithoutNotify(true);
}
// 切池后花费阶梯跟着变,刷新花费/次数/按钮
UpdateDrawUI();
}
/// <summary>取当前激活的 toggle 面板(都没有激活时取第一个)</summary>
private ChouJiangToggle GetActiveToggle()
{
if (toggles == null || toggles.Count == 0) return null;
for (int i = 0; i < toggles.Count; i++)
{
if (toggles[i] != null && toggles[i].gameObject.activeSelf)
return toggles[i];
}
return toggles[0];
}
/// <summary>活动信息显示</summary>
private void UpdateActivityUI()
{
if (config == null) return;
if (tmp_ActivityName != null)
tmp_ActivityName.text = config.activityName;
if (tmp_EndTime != null)
{
if (!config.isOpen)
tmp_EndTime.text = "活动未开启";
else if (config.endTime > 0)
tmp_EndTime.text = "结束时间 " + DateTimeOffset
.FromUnixTimeSeconds(config.endTime).LocalDateTime.ToString("yyyy/MM/dd HH:mm");
else
tmp_EndTime.text = "长期活动";
}
}
/// <summary>花费/次数/按钮状态显示</summary>
private void UpdateDrawUI()
{
int drawn = userStatus != null ? userStatus.totalDrawCount : 0;
if (tmp_Cost != null)
{
long cost = GetNextCost();
tmp_Cost.text = costPayType switch
{
1 => $"<sprite name=\"金币\">{cost}",
3 => $"¥{cost}",
_ => $"<sprite name=\"钻石\">{cost}",
};
}
if (tmp_DrawCount != null)
tmp_DrawCount.text = config != null && config.userMaxDraw > 0
? $"{drawn}/{config.userMaxDraw}"
: drawn.ToString();
UpdateDrawButtonState();
}
private void UpdateDrawButtonState()
{
if (btn_Draw != null)
btn_Draw.interactable = CanDraw() && !isDrawing;
}
/// <summary>下一次抽奖花费(文档规则):
/// 价格数组 = costsByPool[String(poolType)],当前次数 = drawCountByPool[key](不存在按 0),
/// 价格下标 = min(次数, 数组长度-1)。
/// 当前奖池没有价格数组或为空 → 该奖池不可抽奖,不回退其他奖池价格(返回 0)。</summary>
private long GetNextCost()
{
if (config == null || config.costsByPool == null) return 0;
string key = ((int)currPoolType).ToString();
if (!config.costsByPool.TryGetValue(key, out var costs) || costs == null || costs.Count == 0)
return 0;
int drawn = GetPoolDrawCount(key);
int idx = Mathf.Min(drawn, costs.Count - 1);
return costs[idx];
}
/// <summary>当前池是否有价格配置(没有 = 该池不可抽奖,文档明确不允许回退其他池价格)</summary>
private bool HasPoolCost()
{
if (config == null || config.costsByPool == null) return false;
string key = ((int)currPoolType).ToString();
return config.costsByPool.TryGetValue(key, out var costs) && costs != null && costs.Count > 0;
}
/// <summary>当前池已抽次数(价格阶梯索引:少一个奖励价格升一档):
/// drawCountByPool[key](该池独立计数;字典里没有该 key = 该池还没抽过 = 0,
/// 不回退总次数,否则在别的池抽的次数会污染本池价格);
/// 字段整个为 null 才是旧数据,回退总次数</summary>
private int GetPoolDrawCount(string key)
{
if (userStatus == null) return 0;
if (userStatus.drawCountByPool == null) return userStatus.totalDrawCount; // 旧数据无分池计数
return userStatus.drawCountByPool.TryGetValue(key, out var c) ? c : 0;
}
/// <summary>能否再抽:活动开启、在时间内、当前池有价格配置、未抽完、未达次数上限</summary>
private bool CanDraw()
{
if (config == null || !config.isOpen) return false;
if (!HasPoolCost()) return false; // 当前池无价格 → 该池不可抽(不回退其他池)
if (userStatus != null && userStatus.isFinished) return false;
long now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
if (config.startTime > 0 && now < config.startTime) return false;
if (config.endTime > 0 && now > config.endTime) return false;
if (config.userMaxDraw > 0 && userStatus != null
&& userStatus.totalDrawCount >= config.userMaxDraw) return false;
return true;
}
/// <summary>取当前展示/抽奖的分组 toggle 面板</summary>
private ChouJiangToggle GetCurrentToggle()
{
if (toggles == null) return null;
for (int i = 0; i < toggles.Count; i++)
{
if (toggles[i] != null && toggles[i].poolType == currPoolType)
return toggles[i];
}
return null;
}
/// <summary>已抽中奖项兜底重抽的最大次数(服务端正常排除时不会触发)</summary>
private const int MaxDrawRetries = 3;
/// <summary>抽奖:GET /api/Lottery/Draw?poolType=当前分组,结果出来后跑马灯停在抽中的格子</summary>
private void OnDrawClick()
{
if (isDrawing || !CanDraw()) return;
isDrawing = true;
UpdateDrawButtonState();
bool netDone = false; // 网络完成
bool effectDone = true; // 动效完成(默认无动效即完成)
DrawWithRetry(0, res =>
{
netDone = true;
if (this == null || res == null) return;
// 记下本次抽中,同一轮后续抽奖不再要它
drawnIds.Add(res.prizeId);
// 抽中结果(结算 UI 摆好后在这里接入)
Debug.Log($"[ChouJiangPanel] 抽中:{res.name} x{res.count}(type={res.type},消耗={res.cost},分组={(LotteryPoolType)res.poolType})");
// 跑马灯动效:跳到抽中的格子上停住,结束后刷新状态解锁按钮(方块预制体在各 ChouJiangItem 格子上)
ChouJiangToggle cur = GetCurrentToggle();
if (cur != null)
{
effectDone = false;
cur.PlayDrawEffect(res.prizeId, () =>
{
if (this == null) return;
effectDone = true;
isDrawing = false;
// 文档第7-8步:重新请求 UserStatus,刷新次数/价格/置灰(drawnPrizeIds 一并返回)
RefreshUserStatus();
UpdateDrawButtonState();
ShowReward(res); // 领取奖励:弹获得面板
});
}
// 没有当前面板或动效没启动(没预制体等)时直接解锁
if (effectDone)
{
isDrawing = false;
RefreshUserStatus();
UpdateDrawButtonState();
ShowReward(res);
}
});
// code!=0 时 APIManager 只弹错误不回调,超时兜底解锁防连点锁死(网络+动效时间)
StartCoroutine(IEWaitDrawDone(() => netDone && effectDone));
}
/// <summary>
/// 领取奖励:用 UIManager.ShowHuoDe 弹获得面板。
/// type:1=金币 2=钻石 3=道具 4=道具碎片(碎片按道具展示)。
/// Draw 接口不返回 product/icon,用 prizeId 回活动配置里查奖项详情。
/// </summary>
private void ShowReward(LotteryDrawResponse res)
{
if (res == null) return;
bool isProp = res.type == 3 || res.type == 4; // 道具/碎片走下载图标
LotteryPrize prize = isProp ? FindPrizeById(res.prizeId) : null;
// icon 优先(实际数据是完整 URL),product.imageUrl 兜底;相对路径才拼 baseURL,避免拼出双地址
string imgUrl = prize != null
? (string.IsNullOrEmpty(prize.icon) ? prize.product?.imageUrl : prize.icon)
: "";
if (!string.IsNullOrEmpty(imgUrl) && !imgUrl.StartsWith("http"))
imgUrl = APIManager.baseURL + imgUrl;
var data = new List<BackPackPropItemData>
{
new BackPackPropItemData
{
giftType = isProp ? 3 : res.type,
count = (int)res.count,
url = isProp ? imgUrl : "",
propEnum = isProp ? (PropEnum)(prize?.product?.type ?? 0) : PropEnum.none,
},
};
UIManager.Instance.ShowHuoDe(data);
}
/// <summary>用 prizeId 在活动配置的所有奖池里找奖项详情</summary>
private LotteryPrize FindPrizeById(string prizeId)
{
if (config == null || config.pools == null || string.IsNullOrEmpty(prizeId)) return null;
for (int i = 0; i < config.pools.Count; i++)
{
LotteryPoolLevel pool = config.pools[i];
if (pool == null || pool.prizeList == null) continue;
for (int j = 0; j < pool.prizeList.Count; j++)
{
if (pool.prizeList[j] != null && pool.prizeList[j].prizeId == prizeId)
return pool.prizeList[j];
}
}
return null;
}
/// <summary>
/// 抽奖(带兜底重抽):已经抽到过的奖项不再要——服务端按设计应排除已抽中的
/// prizeId(swagger:prizeId 用于唯一性控制,同一用户不能重复抽中),
/// 没排除时客户端重抽兜底,最多 MaxDrawRetries 次(注意:每次重抽服务端照常计费扣次数,
/// 根本解决要在后端 Draw 里排除 drawnPrizeIds)。
/// </summary>
private void DrawWithRetry(int attempt, Action<LotteryDrawResponse> onFinal)
{
APIRequestList.LotteryDraw(new LotteryDrawParam { poolType = (int)currPoolType }, res =>
{
if (this == null) return;
if (res != null && !string.IsNullOrEmpty(res.prizeId)
&& drawnIds.Contains(res.prizeId) && attempt < MaxDrawRetries)
{
Debug.LogWarning($"[ChouJiangPanel] 抽到已中过的 {res.prizeId},兜底重抽({attempt + 1}/{MaxDrawRetries})");
DrawWithRetry(attempt + 1, onFinal);
return;
}
onFinal?.Invoke(res);
});
}
/// <summary>等待网络+动效完成(最多 15 秒),结束解锁按钮</summary>
private IEnumerator IEWaitDrawDone(Func<bool> done)
{
float t = 0f;
while (!done() && t < 15f)
{
t += Time.deltaTime;
yield return null;
}
isDrawing = false;
UpdateDrawButtonState();
}
}