488 lines
19 KiB
C#
488 lines
19 KiB
C#
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();
|
||
}
|
||
}
|