This commit is contained in:
2026-09-10 01:07:35 +08:00
parent ebc693831b
commit 2d7dd23d6c
103 changed files with 97788 additions and 84795 deletions
@@ -83,6 +83,7 @@
using Assets.MyGame.Scripts.Net;
using Assets.MyGame.Scripts.Net.API;
using Assets.MyGame.Scripts.Net.API.GameConfig_SubClass;
using Assets.MyGame.Scripts.Net.API.UserData_SubClass;
using Assets.MyGame.Scripts.Net.APIResponses.Enum;
using Assets.MyGame.Scripts.Net.WebResponses;
@@ -122,6 +123,9 @@ public class GameMainPanel_FishBase : GameMainPanel_Base
public GameTypeEnum gameType;
[Header("Left")]
public PropConfigData propConfigData; // 道具菜单配置(Assets右键 Create → MyGame → 左侧道具配置)
[Tooltip("鱼雷特效配置(按道具编号选爆炸特效预制体,多场景共用一份 .asset;未配置回退纯逻辑版倒计时爆炸)")]
public TorpedoEffectConfig torpedoEffectConfig;
public Button btn_Prop;
public GameObject obj_PropItemParent;
public GameObject obj_PropPanel;
@@ -611,8 +615,8 @@ public class GameMainPanel_FishBase : GameMainPanel_Base
// ⭐ 数量是动态数据:每次打开面板都刷新一级角标(构建有一次性守卫,刷新无守卫)
RefreshPropFirstLayerCounts();
}
else if (tmp_PropTwoPanel != null)
tmp_PropTwoPanel.SetActive(false);
else
ClosePropTwo(); // 关道具面板时一并收起二层并重置展开状态
});
// ⭐ 大喇叭:开/关弹幕输入面板
@@ -1530,48 +1534,127 @@ public class GameMainPanel_FishBase : GameMainPanel_Base
SetPropItemIcon(item, cfg.icon);
var data = cfg; // 闭包捕获当前项,避免循环变量被覆盖
var btn = GetPropItemButton(item);
if (btn != null) btn.onClick.AddListener(() => ShowPropTwo(data));
if (btn != null) btn.onClick.AddListener(() => ShowPropTwo(data, item));
_propFirstLayerItems.Add(new KeyValuePair<PropItemData, GameObject>(cfg, item));
}
RefreshPropFirstLayerCounts();
}
/// <summary>刷新一级道具 child0 数量:仅当该类型下只有 1 个已拥有道具(点击直接使用、不弹二层)时显示个数;
/// 可以打开二层的一级项(0 个显示"暂无道具"占位 / 多个展开子道具列表)直接显示空</summary>
/// <summary>当前展开二层的一级配置(用于"再点一下同一个一级项关闭二层"的开关切换)</summary>
private PropItemData _currentPropTwoCfg;
/// <summary>刷新一级道具 child0 数量:该类型在道具表里只有 1 个道具(只有第一层、点击直达使用)时直接显示具体数量(为 0 就显示 0);
/// 能展开二层的一级项(道具表里有多个道具)显示空</summary>
private void RefreshPropFirstLayerCounts()
{
foreach (var kv in _propFirstLayerItems)
{
if (kv.Value == null) continue; // 克隆项已被销毁
var matched = GetMatchedProps(kv.Key != null ? kv.Key.propType : PropEnum.none);
// ⭐ 只有 1 个已拥有道具(点击直达使用)才显示个数;能弹二层的显示空
SetPropItemTextAt(kv.Value, 0, matched.Count == 1 ? matched[0].count.ToString() : string.Empty);
var twoProps = GetMatchedTwoProps(kv.Key != null ? kv.Key.propType : PropEnum.none);
// ⭐ 只有一层的显示具体数量(数量为 0 就显示 0,不置空);能弹二层的显示空
string text;
if (twoProps.Count == 1)
{
var backend = FindBackendProp(twoProps[0]);
text = backend != null ? Mathf.Max(0, backend.count).ToString() : "0";
}
else
{
text = string.Empty;
}
SetPropItemTextAt(kv.Value, 0, text);
}
}
/// <summary>按类型读取后端已拥有道具(none = 全部类型),只取 count &gt; 0 的(一级角标与二级展开共用同一过滤规则)</summary>
private List<BaseProductCount> GetMatchedProps(PropEnum type)
/// <summary>按类型读取后端道具表(GameManager.gameConfig.propConfig,none = 全部):不读背包,数量为 0 的也显示;未启用(isEnable=false)的剔除</summary>
private List<PropInfConfig> GetMatchedTwoProps(PropEnum type)
{
var matched = new List<BaseProductCount>();
var props = GameManager.Instance.user?.props;
if (props == null) return matched;
foreach (var p in props)
var matched = new List<PropInfConfig>();
var propConfig = GameManager.Instance?.gameConfig?.propConfig;
if (propConfig == null) return matched;
foreach (var t in propConfig)
{
if (p == null || p.count <= 0) continue;
if (type != PropEnum.none && p.type != (int)type) continue;
matched.Add(p);
if (t == null || t.isEnable == false) continue;
if (type != PropEnum.none && t.type != (int)type) continue;
matched.Add(t);
}
return matched;
}
/// <summary>按道具编号在后端背包(user.props)中查找已拥有道具(数量读取与使用判定共用;未拥有返回 null)</summary>
private BaseProductCount FindBackendProp(PropInfConfig t)
{
if (t == null || string.IsNullOrEmpty(t.number)) return null;
var props = GameManager.Instance.user?.props;
if (props == null) return null;
foreach (var p in props)
{
if (p != null && p.number == t.number) return p; // 编号精确匹配
}
return null;
}
/// <summary>关闭二级面板并重置展开状态(点同一个一级项再点一下 / 关闭道具面板时调用)</summary>
private void ClosePropTwo()
{
if (tmp_PropTwoPanel != null) tmp_PropTwoPanel.SetActive(false);
_currentPropTwoCfg = null;
}
/// <summary>重建当前展开的二级列表(道具使用后数量变化时刷新显示,不改变开关状态)</summary>
private void RebuildPropTwo()
{
if (_currentPropTwoCfg == null || tmp_PropTwoPanel == null || !tmp_PropTwoPanel.activeSelf) return;
BuildPropTwoContent(GetMatchedTwoProps(_currentPropTwoCfg.propType));
}
/// <summary>
/// 点击一级道具:清空后按配置的 propType 读取后端已拥有道具(user.props),
/// 动态展开到二级 tmp_PropTwoPanel(每次点击都重建,保证数量/种类最新)
/// 点击一级道具:按 propType 读取后端道具表(gameConfig.propConfig,不读背包,数量为 0 也显示),
/// 展开到 tmp_PropTwoPanel —— 再点一下同一个一级项关闭;点不同的一级项切换内容并重新对齐 Y。
/// 该类型道具表里只有 1 个道具且拥有数量 &gt; 0 时不弹二层,直接触发使用效果。
/// </summary>
private void ShowPropTwo(PropItemData cfg)
private void ShowPropTwo(PropItemData cfg, GameObject firstItem)
{
if (cfg == null || tmp_PropTwoPanel == null || tmp_PropTwoItem == null || tmp_PropTwoItemParent == null) return;
var matched = GetMatchedTwoProps(cfg.propType);
// ⭐ 该类型只配了 1 个二级道具且拥有数量 > 0:不用弹二层,直接触发使用效果
if (matched.Count == 1)
{
var single = FindBackendProp(matched[0]);
if (single != null && single.count > 0)
{
ClosePropTwo(); // 顺带关掉可能开着的其它二层
UsePropEffect(single);
return;
}
}
// ⭐ 再点一下同一个一级项:关闭二层(点不同的一级项则切换内容)
if (_currentPropTwoCfg == cfg && tmp_PropTwoPanel.activeSelf)
{
ClosePropTwo();
return;
}
_currentPropTwoCfg = cfg;
BuildPropTwoContent(matched);
// ⭐ 二层面板 Y 坐标与被点击的一级项对齐(世界坐标,不同父节点下也正确)
if (firstItem != null)
{
var pp = tmp_PropTwoPanel.transform.position;
pp.y = firstItem.transform.position.y;
tmp_PropTwoPanel.transform.position = pp;
}
tmp_PropTwoPanel.SetActive(true);
}
/// <summary>清空并按 gameConfig 道具表重建 tmp_PropTwoItemParent 下的子道具列表(数量从背包按 number 匹配,0 也显示)</summary>
private void BuildPropTwoContent(List<PropInfConfig> matched)
{
// 清空上次克隆的子道具(模板本身若是该父对象的子节点则跳过)
for (int i = tmp_PropTwoItemParent.transform.childCount - 1; i >= 0; i--)
{
@@ -1580,37 +1663,39 @@ public class GameMainPanel_FishBase : GameMainPanel_Base
Destroy(child);
}
// ⭐ 读取后端道具:按一级道具配置的类型过滤(none = 显示全部),只显示 count > 0 的
// (与一级角标 RefreshPropFirstLayerCounts 共用 GetMatchedProps,规则一致)
var matched = GetMatchedProps(cfg.propType);
// ⭐ 只有 1 个时不用弹二层,直接触发该道具的使用效果
if (matched.Count == 1)
{
UsePropEffect(matched[0]);
return;
}
foreach (var p in matched)
foreach (var t in matched)
{
var item = Instantiate(tmp_PropTwoItem, tmp_PropTwoItemParent.transform);
// ⭐ child0 = 名字,child1 = 数量(tmp_PropTwoItem 的 child1 是数量 TMP)
SetPropItemText(item, p.name);
SetPropItemTextAt(item, 1, p.count.ToString());
SetPropItemIconUrl(item, p.imageUrl);
var data = p; // 闭包捕获当前项
var backend = FindBackendProp(t);
int count = backend != null ? Mathf.Max(0, backend.count) : 0;
// ⭐ child0 = 名字,child1 = 数量(读 gameConfig 道具表,数量为 0 也显示)
SetPropItemText(item, t.name);
SetPropItemTextAt(item, 1, count.ToString());
SetPropTwoIcon(item, t); // sprite_Icon 缓存下载(与商店面板同惯例)
var data = t; // 闭包捕获当前项
var btn = GetPropItemButton(item);
if (btn != null) btn.onClick.AddListener(() => UsePropEffect(data));
if (btn != null) btn.onClick.AddListener(() => UsePropTwoItem(data));
}
// 该类型下没有已拥有道具时给出占位提示
// 该类型下道具表里没有任何道具时给出占位提示
if (matched.Count == 0)
{
var empty = Instantiate(tmp_PropTwoItem, tmp_PropTwoItemParent.transform);
SetPropItemText(empty, "暂无道具");
}
}
tmp_PropTwoPanel.SetActive(true);
/// <summary>点击二级道具:实时从背包取数量,数量不足提示,足够则按后端道具触发使用效果</summary>
private void UsePropTwoItem(PropInfConfig t)
{
var backend = FindBackendProp(t);
if (backend == null || backend.count <= 0)
{
var n = t != null && !string.IsNullOrEmpty(t.name) ? t.name : "道具";
UIManager.Instance.ShowBoardError($"[{n}] 数量不足,暂无法使用");
return;
}
UsePropEffect(backend);
}
/// <summary>
@@ -1635,24 +1720,53 @@ public class GameMainPanel_FishBase : GameMainPanel_Base
case PropEnum.VIP经验:
// 游戏内简化为一次使用1个(背包里支持滑条选数量)
var typeName = string.IsNullOrEmpty(p.name) ? PropEnum.VIP经验.ToString() : p.name;
UIManager.Instance.ShowTiShi($"确定使用1个{typeName}吗?", () =>
APIRequestList.UseVipExp(1, (result) =>
{
APIRequestList.UseVipExp(1, (result) =>
UIManager.Instance.ShowBoardError(string.IsNullOrEmpty(result) ? "使用成功" : result);
// 刷新用户数据,同步道具数量与VIP经验
APIRequestList.GetUser((user) =>
{
UIManager.Instance.ShowBoardError(string.IsNullOrEmpty(result) ? "使用成功" : result);
// 刷新用户数据,同步道具数量与VIP经验
APIRequestList.GetUser((user) =>
{
if (user != null)
GameManager.Instance.UpdataUserData(user);
RefreshPropFirstLayerCounts(); // 数量变化后同步刷新一级角标
});
if (user != null)
GameManager.Instance.UpdataUserData(user);
RefreshPropFirstLayerCounts(); // 数量变化后同步刷新一级角标
RebuildPropTwo(); // 二层开着的话同步刷新显示数量
});
});
//UIManager.Instance.ShowTiShi($"确定使用1个{typeName}吗?", () =>
//{
// APIRequestList.UseVipExp(1, (result) =>
// {
// UIManager.Instance.ShowBoardError(string.IsNullOrEmpty(result) ? "使用成功" : result);
// // 刷新用户数据,同步道具数量与VIP经验
// APIRequestList.GetUser((user) =>
// {
// if (user != null)
// GameManager.Instance.UpdataUserData(user);
// RefreshPropFirstLayerCounts(); // 数量变化后同步刷新一级角标
// RebuildPropTwo(); // 二层开着的话同步刷新显示数量
// });
// });
//});
break;
case PropEnum.改名卡:
UIManager.Instance.ShowBoardError("改名卡请到主界面背包中使用");
break;
case PropEnum.鱼雷:
// ⭐ 鱼雷:房间 WebSocket 使用(服务端扣库存+权重随机金币+直接加钱),
// 爆奖表现等 TorpedoResultNotice 房间广播回来(OnTorpedoResult)
var torpedoName = string.IsNullOrEmpty(p.name) ? PropEnum.鱼雷.ToString() : p.name;
WebSocketSendList.UseTorpedo(
GameManager.Instance.gameData.currRoomData.roomId,
GameManager.Instance.user.id,
p.number);
//UIManager.Instance.ShowTiShi($"确定使用1个{torpedoName}吗?", () =>
//{
// WebSocketSendList.UseTorpedo(
// GameManager.Instance.gameData.currRoomData.roomId,
// GameManager.Instance.user.id,
// p.number);
//});
break;
default:
// 装备/碎片等道具没有游戏内使用流程
UIManager.Instance.ShowBoardError($"[{(PropEnum)p.type}] 暂不支持在游戏内使用");
@@ -1660,19 +1774,98 @@ public class GameMainPanel_FishBase : GameMainPanel_Base
}
}
/// <summary>克隆项图标:按后端 imageUrl 下载(与背包面板同惯例,回调时判空防已销毁)</summary>
private void SetPropItemIconUrl(GameObject item, string imageUrl)
/// <summary>
/// 服务端 TorpedoResultNotice 房间广播回调:有玩家使用鱼雷成功(服务端已扣库存+加金币)。
/// 所有客户端都播鱼雷爆奖流程(与 Boss 爆奖 RoomKillNotice 全端实例化同惯例):
/// 随机炸死场上小鱼 + 红包分包爆金币 + 分数面板滚分 + 飞回使用者座位结算(isSelf 决定金/银币)。
/// </summary>
public void OnTorpedoResult(TorpedoResultNotice data)
{
if (string.IsNullOrEmpty(imageUrl)) return;
if (data == null) return;
// 使用者座位(可能是本地玩家,也可能是其他人)
SitPosData sitData = null;
foreach (var sit in list_Sits)
{
if (sit != null && sit.user == data.userId) { sitData = sit; break; }
}
if (sitData == null)
{
Debug.LogWarning($"[鱼雷] 未找到使用者座位: userId={data.userId}");
return;
}
// JiaFen:结算时金币回加到使用者座位显示(服务端已加钱,这里只是表现层,与杀鱼 onScoreAdd 同惯例)
long gold = data.gold;
var targetSit = sitData;
Action<int> jiaFen = (audioID) =>
{
targetSit.goldNum += gold;
if (targetSit.tMP_GoldNum != null)
targetSit.tMP_GoldNum.text = targetSit.goldNum.ToString();
AudioManager.Instance.PlayerFX(audioID);
};
// 鱼雷颜色(后端下发的十六进制色值,特效版用来渲染爆炸特效;解析失败用默认色)
Color torpedoColor = Color.white;
if (!string.IsNullOrEmpty(data.color)) ColorUtility.TryParseHtmlString(data.color, out torpedoColor);
// ⭐ 按鱼雷编号选择特效预制体(torpedoEffectConfig 里配编号→prefab;
// 未配置的编号回退纯逻辑版:内置倒计时直接爆炸)
TorpedoEvent evt = null;
if (torpedoEffectConfig != null && torpedoEffectConfig.effects != null)
{
foreach (var e in torpedoEffectConfig.effects)
{
if (e == null || e.effectPrefab == null || e.propNumber != data.propNumber) continue;
var fxGo = Instantiate(e.effectPrefab);
// prefab 忘挂组件时兜底补挂(必须与动画同一对象,旧版动画事件才调得到)
evt = fxGo.GetComponent<TorpedoEvent>();
if (evt == null) evt = fxGo.AddComponent<TorpedoEvent>();
break;
}
}
if (evt == null)
{
// 回退:纯逻辑版(无特效,内置倒计时→爆炸)
var go = new GameObject("TorpedoEvent");
evt = go.AddComponent<TorpedoEvent>();
}
evt.torpedoColor = torpedoColor;
evt.FireTorpedo(data.gold, sitData.posID, jiaFen);
// 本地玩家使用成功:刷新用户数据(鱼雷库存已扣)+ 道具菜单数量
if (data.userId == GameManager.Instance.user.id)
{
APIRequestList.GetUser((user) =>
{
if (user != null)
GameManager.Instance.UpdataUserData(user);
RefreshPropFirstLayerCounts(); // 鱼雷库存已扣,刷新一级角标
RebuildPropTwo(); // 二层开着的话同步刷新数量
});
}
}
/// <summary>二级道具图标:优先用 gameConfig 缓存的 sprite_Icon,未缓存则按 imageUrl 下载并回填缓存(与商店面板同惯例,回调时判空防已销毁)</summary>
private void SetPropTwoIcon(GameObject item, PropInfConfig t)
{
if (t == null) return;
var img = item.transform.childCount > 0 ? item.transform.GetChild(0).GetComponent<Image>() : null;
if (img == null) img = item.GetComponent<Image>();
if (img == null) img = item.GetComponentInChildren<Image>();
if (img == null) return;
WebTool.DownImage(APIManager.baseURL + imageUrl, (texture) =>
if (t.sprite_Icon != null)
{
if (item == null || img == null) return;
if (texture != null)
img.sprite = Sprite.Create(texture, new Rect(0, 0, texture.width, texture.height), Vector2.one * 0.5f);
img.sprite = t.sprite_Icon;
return;
}
if (string.IsNullOrEmpty(t.imageUrl)) return;
WebTool.DownImage(APIManager.baseURL + t.imageUrl, (texture) =>
{
if (item == null || texture == null) return;
t.sprite_Icon = Sprite.Create(texture, new Rect(0, 0, texture.width, texture.height), Vector2.one * 0.5f);
img.sprite = t.sprite_Icon;
});
}