This commit is contained in:
2026-09-23 19:33:45 +08:00
parent af7a380e18
commit 29ce608348
16 changed files with 2575 additions and 676 deletions
File diff suppressed because it is too large Load Diff
@@ -18,6 +18,9 @@ public class ChouJiangItem : MonoBehaviour
[Tooltip("本格子的位置编号 = prizeId 后两位(如 588203 → 3),初始化时奖品按它对号入座")]
public int itemId;
[Tooltip("本格子的跑马灯方块预制体:抽奖动效经过/停在本格子时点亮,大小自动设成格子(背景)大小")]
public GameObject prefab_DrawEffect;
[Header("动态生成参数")]
[Tooltip("图标占格子短边的比例")]
public float iconRatio = 0.85f;
@@ -44,6 +47,7 @@ public class ChouJiangItem : MonoBehaviour
private Image img_Icon; // 动态生成的图标
private TextMeshProUGUI tmp_Count; // 动态生成的数量文本
private GameObject maskObj; // 动态生成的遮罩蒙版(不规则裁剪用)
private GameObject effectObj; // 本格子的跑马灯方块实例(懒创建)
// 金币/钻石本地图标(Resources 根目录,与 BackPackPropItem 同一套),静态缓存避免重复加载
private static Sprite _goldSprite;
@@ -92,6 +96,33 @@ public class ChouJiangItem : MonoBehaviour
img_Icon.color = drawn ? drawnColor : Color.white;
}
/// <summary>
/// 显示/隐藏本格子的跑马灯方块:第一次点亮时用 prefab_DrawEffect 创建实例,
/// 大小自动设成格子(背景)大小、压在最上层。
/// </summary>
public void ShowEffect(bool show)
{
if (show && effectObj == null && prefab_DrawEffect != null)
{
RectTransform rt = transform as RectTransform;
if (rt != null)
{
effectObj = Instantiate(prefab_DrawEffect, rt, false);
RectTransform er = effectObj.transform as RectTransform;
if (er != null)
{
er.anchorMin = er.anchorMax = er.pivot = Vector2.one * 0.5f;
er.anchoredPosition = Vector2.zero;
er.localScale = Vector3.one;
er.sizeDelta = new Vector2(rt.rect.width, rt.rect.height); // 方块大小 = 背景大小
}
effectObj.transform.SetAsLastSibling();
}
}
if (effectObj != null)
effectObj.SetActive(show);
}
/// <summary>清掉动态生成的 UI(重复 Init 复用前调用)</summary>
public void ClearUI()
{
@@ -49,11 +49,16 @@ public class ChouJiangPanel : SubUIBase
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()
@@ -132,6 +137,11 @@ public class ChouJiangPanel : SubUIBase
{
if (this == null) return;
userStatus = status;
if (status != null && status.drawnPrizeIds != null)
{
for (int i = 0; i < status.drawnPrizeIds.Count; i++)
drawnIds.Add(status.drawnPrizeIds[i]);
}
UpdateDrawUI();
});
}
@@ -142,6 +152,14 @@ public class ChouJiangPanel : SubUIBase
APIRequestList.GetLotteryDrawnPrizes(ids =>
{
if (this == null || toggles == null) return;
drawnIds.Clear();
if (ids != null)
{
for (int i = 0; i < ids.Count; i++)
drawnIds.Add(ids[i]);
}
for (int i = 0; i < toggles.Count; i++)
{
if (toggles[i] != null)
@@ -218,7 +236,15 @@ public class ChouJiangPanel : SubUIBase
int drawn = userStatus != null ? userStatus.totalDrawCount : 0;
if (tmp_Cost != null)
tmp_Cost.text = GetNextCost().ToString();
{
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
@@ -259,36 +285,150 @@ public class ChouJiangPanel : SubUIBase
return true;
}
/// <summary>抽奖:GET /api/Lottery/Draw?poolType=当前分组</summary>
/// <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 done = false;
APIRequestList.LotteryDraw(new LotteryDrawParam { poolType = (int)currPoolType }, res =>
bool netDone = false; // 网络完成
bool effectDone = true; // 动效完成(默认无动效即完成)
DrawWithRetry(0, res =>
{
done = true;
if (this == null) return;
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})");
// 花费随次数增长、已抽奖项要置灰,重拉状态刷新
RefreshUserStatus();
RefreshDrawnPrizes();
// 跑马灯动效:跳到抽中的格子上停住,结束后刷新状态解锁按钮(方块预制体在各 ChouJiangItem 格子上)
ChouJiangToggle cur = GetCurrentToggle();
if (cur != null)
{
effectDone = false;
cur.PlayDrawEffect(res.prizeId, () =>
{
if (this == null) return;
effectDone = true;
isDrawing = false;
RefreshUserStatus(); // 花费随次数增长
RefreshDrawnPrizes(); // 已抽奖项置灰
UpdateDrawButtonState();
ShowReward(res); // 领取奖励:弹获得面板
});
}
// 没有当前面板或动效没启动(没预制体等)时直接解锁
if (effectDone)
{
isDrawing = false;
RefreshUserStatus();
RefreshDrawnPrizes();
UpdateDrawButtonState();
ShowReward(res);
}
});
// code!=0 时 APIManager 只弹错误不回调,超时兜底解锁防连点锁死
StartCoroutine(IEWaitDrawDone(() => done));
// code!=0 时 APIManager 只弹错误不回调,超时兜底解锁防连点锁死(网络+动效时间)
StartCoroutine(IEWaitDrawDone(() => netDone && effectDone));
}
/// <summary>等待抽奖回调完成(最多 10 秒),结束解锁按钮</summary>
/// <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 < 10f)
while (!done() && t < 15f)
{
t += Time.deltaTime;
yield return null;
@@ -1,4 +1,6 @@
using Assets.MyGame.Scripts.Net.API.GameConfig_SubClass;
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
@@ -21,6 +23,16 @@ public class ChouJiangToggle : MonoBehaviour
[Tooltip("C级(小奖)item,按展示顺序拖")]
public List<ChouJiangItem> items_C;
[Header("抽奖动效参数")]
[Tooltip("跑马灯圈数:跳完这些圈再停到目标格")]
public int effectLoops = 2;
[Tooltip("单步最快间隔(秒)")]
public float effectStepFast = 0.07f;
[Tooltip("单步最慢间隔(秒):开头/结尾慢,中间快")]
public float effectStepSlow = 0.30f;
private Coroutine effectCo; // 跑马灯协程
/// <summary>
/// 初始化:筛出本组(poolType)的全部奖池,同等级合并后,
/// A/B/C 级的奖品各自按 prizeId 后两位(itemId)找到自己的格子。
@@ -60,11 +72,38 @@ public class ChouJiangToggle : MonoBehaviour
if (pool.prizeList != null) list.AddRange(pool.prizeList);
}
// 兜底:没手填 itemId 的列表按拖入顺序自动编 1..n(手填过的列表不受影响)
AutoNumberItems(items_A);
AutoNumberItems(items_B);
AutoNumberItems(items_C);
FillLevel("A", items_A, prizesByLevel);
FillLevel("B", items_B, prizesByLevel);
FillLevel("C", items_C, prizesByLevel);
}
/// <summary>
/// 兜底编号:整列表都没手填 itemId(全为 0)时按列表拖入顺序自动编 1..n;
/// 只要有一个格子手填过编号,该列表就完全按手填的来。
/// </summary>
private static void AutoNumberItems(List<ChouJiangItem> items)
{
if (items == null || items.Count == 0) return;
bool anySet = false;
for (int i = 0; i < items.Count; i++)
{
if (items[i] != null && items[i].itemId != 0) { anySet = true; break; }
}
if (anySet) return;
for (int i = 0; i < items.Count; i++)
{
if (items[i] != null) items[i].itemId = i + 1;
}
Debug.Log($"[ChouJiangToggle] 列表未手填 itemId,按拖入顺序自动编号 1..{items.Count}");
}
/// <summary>按已抽奖项列表置灰本面板所有 item(奖励不重复)</summary>
public void RefreshDrawnState(List<string> drawnPrizeIds)
{
@@ -131,6 +170,120 @@ public class ChouJiangToggle : MonoBehaviour
}
}
// ============================================================
#region 抽奖跑马灯动效
// ============================================================
/// <summary>
/// 播放抽奖动效:跑马灯在格子间 加速→匀速→减速 跳动,最后停在 prizeId 对应的格子上。
/// 方块是各格子自己的 prefab_DrawEffect(ChouJiangItem 上拖),经过/停留时点亮。
/// 完成后回调 onComplete;没有任何格子配方块/找不到目标格时立即回调。
/// </summary>
public void PlayDrawEffect(string prizeId, Action onComplete)
{
// 停掉上一次
if (effectCo != null) { StopCoroutine(effectCo); effectCo = null; }
List<ChouJiangItem> path = BuildEffectPath();
ChouJiangItem target = FindItemByPrizeId(prizeId);
bool anyEffect = false;
for (int i = 0; i < path.Count; i++)
{
if (path[i].prefab_DrawEffect != null) { anyEffect = true; break; }
}
if (!anyEffect || path.Count == 0 || target == null || !path.Contains(target))
{
onComplete?.Invoke();
return;
}
effectCo = StartCoroutine(IEPlayEffect(path, target, onComplete));
}
/// <summary>动效路径:A → B → C 三个列表的格子,各自按 itemId 升序(只含有奖品且未抽中的格子——已经抽到的不再跳)</summary>
private List<ChouJiangItem> BuildEffectPath()
{
List<ChouJiangItem> path = new List<ChouJiangItem>();
AppendPath(items_A, path);
AppendPath(items_B, path);
AppendPath(items_C, path);
return path;
}
private static void AppendPath(List<ChouJiangItem> items, List<ChouJiangItem> path)
{
if (items == null) return;
List<ChouJiangItem> sorted = new List<ChouJiangItem>();
for (int i = 0; i < items.Count; i++)
{
ChouJiangItem it = items[i];
if (it != null && it.prize != null && !it.IsDrawn) sorted.Add(it); // 已抽中(置灰)的跳过
}
sorted.Sort((a, b) => a.itemId.CompareTo(b.itemId));
path.AddRange(sorted);
}
/// <summary>按 prizeId 找本面板的 item</summary>
private ChouJiangItem FindItemByPrizeId(string prizeId)
{
if (string.IsNullOrEmpty(prizeId)) return null;
ChouJiangItem r = FindInList(items_A, prizeId);
if (r == null) r = FindInList(items_B, prizeId);
if (r == null) r = FindInList(items_C, prizeId);
return r;
}
private static ChouJiangItem FindInList(List<ChouJiangItem> items, string prizeId)
{
if (items == null) return null;
for (int i = 0; i < items.Count; i++)
{
if (items[i] != null && items[i].prize != null && items[i].prize.prizeId == prizeId)
return items[i];
}
return null;
}
private IEnumerator IEPlayEffect(List<ChouJiangItem> path, ChouJiangItem target, Action onComplete)
{
int count = path.Count;
int targetIdx = path.IndexOf(target);
int cur = 0;
// 全部先熄灭(含上一次中奖格上亮着的方块),点亮起点
for (int i = 0; i < count; i++) path[i].ShowEffect(false);
path[0].ShowEffect(true);
// 总步数 = 圈数 × 格子数 + 到目标格的距离,保证最后一步正好落在目标格
int totalSteps = effectLoops * count + (targetIdx - cur + count) % count;
for (int step = 0; step < totalSteps; step++)
{
float p = (float)step / totalSteps;
yield return new WaitForSeconds(GetStepDuration(p));
path[cur].ShowEffect(false); // 熄灭当前格
cur = (cur + 1) % count;
path[cur].ShowEffect(true); // 点亮下一格
}
// 停在目标格上停留一下再回调(中奖格的方块保持亮着)
yield return new WaitForSeconds(0.6f);
onComplete?.Invoke();
}
/// <summary>单步时长:前 25% 加速 → 中间匀速 → 后 30% 减速(先快后慢的抽奖手感)</summary>
private float GetStepDuration(float p)
{
if (p < 0.25f) return Mathf.Lerp(effectStepSlow, effectStepFast, p / 0.25f);
if (p < 0.70f) return effectStepFast;
return Mathf.Lerp(effectStepFast, effectStepSlow, (p - 0.70f) / 0.30f);
}
#endregion
private void ClearItems(List<ChouJiangItem> items)
{
if (items == null) return;