1
This commit is contained in:
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;
|
||||
|
||||
Reference in New Issue
Block a user