Files
5.24BuYu/Assets/MyGame/Scripts/Characters/Monster/MonsterControl_BossBase.cs
T
2026-09-18 08:15:08 +08:00

384 lines
15 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.APIResponses.Enum;
using Assets.MyGame.Scripts.Net.WebResponses;
using DG.Tweening;
using System;
using System.Collections;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;
using static MonsterMoveLine;
using Random = UnityEngine.Random;
public class MonsterControl_BossBase : MonsterContral
{
// ==================== 基础 ====================
public string bossName;
// ==================== 爆奖 ====================
[Header("爆奖")]
public bool isToUI = true; // 爆奖特效是 UI(RectTransform)还是 3D 场景物体
public LayeredPool layeredPool;
public TextMeshProUGUI tMP_CurrBossScore; // Boss 分数显示文本
public long currBossScoreValue; // 当前滚动分数
public long bossScoreValue; // 累计总分
public int poolNum; // 奖池金币
public int prizeCount = 8; // 爆奖红包数量
public Action<int> JiaFen; // 加分回调
public List<List<RedPacketRecord>> redPacketRecords = new List<List<RedPacketRecord>>();
public Camera ZhuCam;
// ==================== 死亡 ====================
public bool isDieFish;
public float dieTime = 3f;
public Vector3 v3; // 分数飞出目标位置(Inspector 设置)
private long _turretNum;
private bool _isJia;
/// <summary>是否本地玩家击杀(金/银币样式切换用,由 Ws_DieFish 设置)</summary>
public bool isLocalCoin = false;
/// <summary>击杀者座位 ID(人机击杀时由房主代发,但 sitPos 应为人机座位,而非房主自己)</summary>
public int killerSitPos = -1;
/// <summary>
/// ⭐ Init 异步防污染令牌:async void Init 里 await Addressables 加载期间,
/// Boss 可能被回收并重新生成(对象池复用)。若旧 Init 的 await 完成后继续执行,
/// 会用旧的 pathId 覆盖新 Boss 的 moveLine/位置/isInitialized,导致第二次 Boss 打不死。
/// 每次 Init 递增此令牌,await 后若令牌已变,说明已被新 Init 覆盖,直接 return。
/// </summary>
private int _initToken = 0;
// ==================== Unity 生命周期 ====================
public override void Awake()
{
base.Awake();
//isRotate = false;
ZhuCam = GameManager.Instance.ZhuCam;
action_effect_DieAward = obj =>
{
tMP_CurrBossScore = obj.transform.GetChild(0).GetComponent<TextMeshProUGUI>();
// ⭐ 按 isLocalCoin 切金/银:自己击杀=金币,他人=银币
ApplyBossScoreStyle(obj.transform);
};
}
/// <summary>
/// 按 isLocalCoin 切换 Boss 分数 TMP 的 spriteAsset:
/// 自己击杀 → goldSprite
/// 他人击杀 → silverSprite
/// 优先用 BJImage(新预制体),回退 ScoreNumScrollEvent,再回退直接取 TMP。
/// 需对应 sprite 已在 Inspector 赋值,否则跳过不影响原显示。
/// </summary>
private void ApplyBossScoreStyle(Transform root)
{
if (tMP_CurrBossScore == null) return;
var bjImage = root.GetComponentInChildren<BJImage>();
if (bjImage != null)
{
var spriteAsset = isLocalCoin ? bjImage.goldSprite : bjImage.silverSprite;
if (spriteAsset != null)
tMP_CurrBossScore.spriteAsset = spriteAsset;
return;
}
var scrollEvent = root.GetComponentInChildren<ScoreNumScrollEvent>();
if (scrollEvent != null)
{
scrollEvent.SetScoreStyle(isLocalCoin);
}
}
public override void OnEnable()
{
base.OnEnable();
layeredPool = null;
currBossScoreValue = 0;
bossScoreValue = 0;
redPacketRecords.Clear();
_isJia = false;
isDieFish = false;
isLocalCoin = false;
killerSitPos = -1;
var res = FishResourcesData.fishResourcesDatasDic[fishTypeID];
if (res.showAudio != null && res.showAudio.Count > 0)
AudioManager.Instance.PlayerFX(res.showAudio[Random.Range(0, res.showAudio.Count)]);
}
public override void OnDisable()
{
base.OnDisable();
if (isDieFish)
GameManager.Instance.StartCoroutine(YanChiDie());
}
// ==================== 初始化 ====================
public async override void Init(string pathId, float time, bool isyuzhen = false, string yzID = "")
{
// ⭐ 递增令牌:await 后检查此令牌,若已变说明被新 Init 覆盖(对象池复用),直接 return
int token = ++_initToken;
isyz = isyuzhen;
this.yzID = yzID;
// ⭐ 缓存 yzID 前缀(Boss 未调 base.Init,需单独缓存)
// 避免 UpdateYZMovement 里 _yzPrefix 为空导致鱼阵 Boss 不移动
SetYzPrefix(yzID);
// ⭐ 缓存路线编号(Boss 未调 base.Init,lastMoveLineId 需单独保存,
// ApplyFacingDirection 的双数路线镜像 routeFlipMirror 按它判定奇偶)
lastMoveLineId = pathId;
// ⭐ Boss朝向:不随路线切线旋转,按路线编号奇偶翻转X轴
// (单数路线=左→右保持预制体正向,双数路线=右→左翻转X轴为负)
if (isMove && IsRouteFlipBoss)
ApplyBossRouteFlip(pathId);
// ⭐ 创建视频处理:配置了 createVideoClip 时通过 GameMainPanel 的播放器播放
// 播放期间 OnMoveUpdata 会跳过移动,视频结束后才开始走路线
_createVideoDuration = 0f;
_isPlayingCreateVideo = false;
var videoPlayer = CreateFishManager.Instance?.currGameType?.createVideoPlayer;
if (createVideoClip != null && videoPlayer != null)
{
// 显示视频控件
videoPlayer.gameObject.SetActive(true);
videoPlayer.Stop();
videoPlayer.clip = createVideoClip;
// ⭐ 注册播放结束回调:视频播完自动隐藏控件 + 重置状态
videoPlayer.loopPointReached -= OnCreateVideoFinished;
videoPlayer.loopPointReached += OnCreateVideoFinished;
videoPlayer.Play();
_isPlayingCreateVideo = true;
}
// Boss 进场播报
if (GameManager.Instance.gameData.currGameType == GameType.XJMH)
{
var handle = Addressables.LoadAssetAsync<Sprite>($"Assets/Art/XJMH/pw/pw_sprite/handbook/{bossName}.png");
await handle.Task;
// ⭐ async void Init 防污染:await 期间 Boss 可能被回收(对象池复用)或销毁(退出房间)。
// 检查顺序很重要:先 this == null(被 Destroy),再 activeInHierarchy(被回收),最后令牌
// Unity 重写 == 运算符:被 Destroy 的 MonoBehaviour 比较 == null 返回 true
if (this == null || token != _initToken || !gameObject.activeInHierarchy)
{
if (this != null)
Debug.Log($"[MonsterControl_BossBase] Init await 完成,但令牌已变或 Boss 已回收/销毁,跳过: {bossName} token={token} curr={_initToken} active={gameObject.activeInHierarchy}");
return;
}
if (CreateFishManager.Instance != null && CreateFishManager.Instance.currGameType != null
&& CreateFishManager.Instance.currGameType.subUIManager != null)
{
CreateFishManager.Instance.currGameType.subUIManager.ShowBoardBoss(handle.Result, bossName);
}
}
if (isMove)
{
var bezierData = MonsterMoveLine.dic_BezierCurveDatas[bossName + pathId];
monsterData.moveLine.Init(bezierData, time);
// ⭐ isMove=true 的 Boss 初始位置设为曲线起点,
// 避免在 moveTime 更新前卡在生成时的 (9999,9999,0)
// z 保留当前值:3D Boss 在预制体上配置的深度不被擦掉
transform.position = new Vector3(bezierData.start.x, bezierData.start.y, transform.position.z);
}
else
{
// x/y 归零;z 不带——用预制体自带的深度
transform.position = new Vector3(0, 0, transform.position.z);
}
// ⭐ Boss 重写 Init 未调用 base.Init,需手动标记已初始化并切换到 move 状态,
// 否则 OnMoveUpdata 不会被调用(OnUpdate 里 isInitialized=false 会 return),moveTime 不自增
isInitialized = true;
ChangeState(CharacterStateType.move);
if (transform.childCount == 0) return;
Transform child = transform.GetChild(0);
Vector3 localPos = child.localPosition;
// ⭐ x/y 归零摆正模型;z 深度只在显式配置 depthOffset(≠0)时才覆盖,
// 否则保留预制体上设置的 3D 深度(3D Boss 用,之前被强制归零导致深度无效)
localPos.x = 0f;
if (depthOffset != 0)
localPos.z = depthOffset;
child.localPosition = localPos;
}
/// <summary>
/// 创建视频播放结束回调:隐藏视频控件 + 重置播放状态
/// (用 loopPointReached 事件,比 OnMoveUpdata 检测更可靠,
/// 因为 isMove=false 的 Boss 不会进入 OnMoveUpdata 的视频检测分支)
/// </summary>
private void OnCreateVideoFinished(UnityEngine.Video.VideoPlayer player)
{
_isPlayingCreateVideo = false;
if (player != null)
{
player.Stop();
player.gameObject.SetActive(false);
}
}
// ==================== 爆奖(公共) ====================
/// <summary>确保分数显示文本存在(自动创建)</summary>
private void EnsureScorePanel()
{
if (tMP_CurrBossScore != null) return;
var prefab = CreateFishManager.Instance.dieFXPrefabAwardTmp;
if (prefab == null) return;
var parent = CreateFishManager.Instance.currGameType.subPanelParent;
if (parent == null) return;
var board = Instantiate(prefab, parent);
tMP_CurrBossScore = board.transform.GetChild(0).GetComponent<TextMeshProUGUI>();
// ⭐ 按 isLocalCoin 切金/银:自己击杀=金币,他人=银币
ApplyBossScoreStyle(board.transform);
}
/// <summary>初始化红包分层对象池</summary>
private void EnsureLayeredPool()
{
if (layeredPool == null)
layeredPool = new LayeredPool(poolNum, prizeCount);
}
/// <summary>爆奖:随机红包(min~max 个)</summary>
public void BigPrize(int min = 4, int max = 8)
{
EnsureScorePanel();
EnsureLayeredPool();
var records = layeredPool.BossGlodCoinRandrom(tMP_CurrBossScore.transform.position, min, max, killerSitPos);
AddRedPacket(records);
}
/// <summary>爆奖:固定数量红包</summary>
public void BigPrizeNotFish(int count)
{
EnsureScorePanel();
EnsureLayeredPool();
var records = layeredPool.BossGlodCoinNotFish(count);
AddRedPacket(records);
}
/// <summary>爆奖:全屏小鱼数量红包</summary>
public void BigPrizeAll()
{
int smallFishCount = CreateFishManager.Instance.fishDatas_hashCodekey_Small.Count;
prizeCount = 1;
EnsureScorePanel();
EnsureLayeredPool();
var records = layeredPool.BossGlodCoin(smallFishCount, tMP_CurrBossScore.transform.position, killerSitPos);
AddRedPacket(records);
}
/// <summary>记录红包并开始分数滚动 + 加分</summary>
private void AddRedPacket(List<RedPacketRecord> records)
{
redPacketRecords.Add(records);
foreach (var r in records)
bossScoreValue += r.Amount;
_turretNum = CreateFishManager.Instance.currGameType.currSitData.consumeGoldNum;
GameManager.Instance.StartCoroutine(ShowBigPrize());
GameManager.Instance.StartCoroutine(JiaFenRoutine());
}
// ==================== 分数滚动 ====================
private IEnumerator ShowBigPrize()
{
if (_isJia) yield break;
_isJia = true;
yield return new WaitForSeconds(1f);
_turretNum = Random.Range((int)(_turretNum / 2), (int)_turretNum + 1);
while (currBossScoreValue < bossScoreValue)
{
currBossScoreValue += _turretNum;
if (currBossScoreValue > bossScoreValue)
currBossScoreValue = bossScoreValue;
tMP_CurrBossScore.text = currBossScoreValue.ToString();
yield return null;
}
_isJia = false;
}
private IEnumerator JiaFenRoutine()
{
yield return new WaitForSeconds(1.5f);
Transform parent = tMP_CurrBossScore.transform.parent;
GameObject addScorePrefab = CreateFishManager.Instance.dieFXPrefabAwardTmp_AddScore;
var records = redPacketRecords[0];
redPacketRecords.RemoveAt(0);
foreach (var item in records)
{
yield return new WaitForSeconds(0.15f);
var inst = Instantiate(addScorePrefab, parent.position + Vector3.up * 60, Quaternion.identity, parent);
// ⭐ 按 isLocalCoin 切金/银:优先用 ScoreNumScrollEvent,回退直接赋文本
var scrollEvent = inst.GetComponent<ScoreNumScrollEvent>();
if (scrollEvent != null)
{
scrollEvent.SetScoreStyle(isLocalCoin);
scrollEvent.SetScoreText(CoinScript.NumAdd(item.Amount.ToString()));
}
else
{
inst.transform.GetChild(0).GetComponent<TextMeshProUGUI>().text = CoinScript.NumAdd(item.Amount.ToString());
}
AudioManager.Instance.PlayerFX(61009);
}
}
// ==================== 死亡 ====================
public override void DieFish(RoomKillNotice obj = null)
{
isDieFish = true;
GameManager.Instance.cameraShake.ShakePresets.Explosion2D(3f, 4f, 1f);
if (obj != null)
poolNum = (int)obj.gold;
var res = FishResourcesData.fishResourcesDatasDic[fishTypeID];
if (res.dieAudio != null && res.dieAudio.Count > 0)
AudioManager.Instance.PlayerFX(res.dieAudio[Random.Range(0, res.dieAudio.Count)]);
}
/// <summary>死亡后延迟飞分到座位</summary>
private IEnumerator YanChiDie()
{
yield return new WaitForSeconds(dieTime);
// not-self 爆奖模式下未实例化分数面板和 effect_DieAward,跳过飞分逻辑
// (加分由 BaoJiangNotSelfHandler.OnBaoJiangEnd 处理)
if (tMP_CurrBossScore == null || tMP_CurrBossScore.transform.parent == null)
yield break;
tMP_CurrBossScore.transform.parent
.DOMove(v3, 0.5f)
.OnComplete(() =>
{
JiaFen?.Invoke(0);
Destroy(tMP_CurrBossScore.transform.parent.gameObject);
});
}
}