1124 lines
40 KiB
C#
1124 lines
40 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.Linq;
|
||
using System.Threading.Tasks;
|
||
using asap.core;
|
||
using cfg;
|
||
using Event1v1Battle.Panel;
|
||
using Event1v1Battle.Panel.MainPanel;
|
||
using Event1v1Battle.Panel.RewardPanel;
|
||
using game;
|
||
using GameCore;
|
||
using Newtonsoft.Json.Linq;
|
||
using SideGameFromFishing;
|
||
using UI.Home.Event;
|
||
using UI.PackPanel;
|
||
using UniRx;
|
||
using UnityEngine;
|
||
using IEventData = Activity.IEventData;
|
||
using Random = UnityEngine.Random;
|
||
using Activity;
|
||
|
||
namespace Event1v1Battle.Data
|
||
{
|
||
/// <summary>
|
||
/// 1v1扇耳光大赛活动管理器
|
||
/// </summary>
|
||
public class Event1v1BattleSlapManager : Activity.AEventDataManager<Event1v1BattleSlapData>, IActivityEventRedPoint<Event1v1BattleSlapData>,
|
||
ISideGameBombRaidProgress, ISideGameFishRaidProgress
|
||
{
|
||
#region 常量
|
||
|
||
private const int SOURCE_BOMB = 0; // 轰炸
|
||
private const int SOURCE_SHOOT = 1; // 射鱼
|
||
private const int SOURCE_FISHING = 2; // 钓鱼
|
||
|
||
#endregion
|
||
|
||
#region 字段
|
||
|
||
private Event1v1BattleSlapPresentationData _presentationData = new();
|
||
|
||
/// <summary>
|
||
/// 获取表现数据
|
||
/// </summary>
|
||
public Event1v1BattleSlapPresentationData PresentationData => _presentationData;
|
||
|
||
#endregion
|
||
|
||
#region 构造函数
|
||
|
||
public Event1v1BattleSlapManager()
|
||
{
|
||
GContext.OnEvent<FishingEndEvent>().Subscribe(OnFishingEnd).AddTo(CompositeDisposable);
|
||
GContext.OnEvent<HomeStartEvent>().Subscribe(OnHomeStart).AddTo(CompositeDisposable);
|
||
}
|
||
|
||
private void OnFishingEnd(FishingEndEvent e)
|
||
{
|
||
var data = LoadData();
|
||
#if UNITY_EDITOR
|
||
if ((!data.IsActive || !data.IsOnGoing) && Tables.TbEventSlapSpinach.TryGetByItemID(e.ItemId, out _))
|
||
{
|
||
Debug.LogError($"有人作弊触发了菠菜,需要匹配对少才能触发正确流程");
|
||
}
|
||
#endif
|
||
if (!data.IsActive || !data.IsOnGoing)
|
||
return;
|
||
|
||
Tables.TbEventSlapSpinach.TryGetByItemID(e.ItemId, out var spinachConfig);
|
||
if (spinachConfig == null) return;
|
||
OnSpinachCaught(data);
|
||
}
|
||
|
||
private void OnHomeStart(HomeStartEvent e)
|
||
{
|
||
var data = LoadData();
|
||
if (CheckAndHandleBattleTimeout(data)) return;
|
||
var needShowNpcAttackTv = CheckAndSimulatePendingAttacks(data, needClear: !_presentationData.IsPlayAttacking);
|
||
if (needShowNpcAttackTv)
|
||
ShowFaceTv(data);
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 初始化
|
||
|
||
#region 业务方法(从DataEx移入)
|
||
|
||
/// <summary>
|
||
/// 活动激活时初始化
|
||
/// </summary>
|
||
public void InitOnActive(Event1v1BattleSlapData data)
|
||
{
|
||
InitRound(data);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 初始化当前轮次
|
||
/// </summary>
|
||
public void InitRound(Event1v1BattleSlapData data)
|
||
{
|
||
data.CurrentStageID = data.InitConfig.InitialStage;
|
||
data.StageHistory?.Clear();
|
||
data.PlayerLifeCount = 3;
|
||
data.SlapPaidReviveCount = 0;
|
||
InitializeStageHp(data);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 初始化当前关卡的HP
|
||
/// </summary>
|
||
public void InitializeStageHp(Event1v1BattleSlapData data)
|
||
{
|
||
var stageConfig = data.StageConfig;
|
||
if (stageConfig == null)
|
||
{
|
||
Debug.LogError($"[EventSlap] Stage config not found for ID: {data.CurrentStageID}");
|
||
return;
|
||
}
|
||
|
||
RandomNpcData(data);
|
||
int maxHp = GetPlayerMaxHp(data);
|
||
data.NpcHp = maxHp;
|
||
data.MaxHp = maxHp;
|
||
data.PlayerHp = maxHp;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取玩家最大HP(关卡表 StageHP 经 <see cref="PlayerItemData.IntInflation"/> 与体力一致的膨胀系数处理)
|
||
/// </summary>
|
||
public int GetPlayerMaxHp(Event1v1BattleSlapData data)
|
||
{
|
||
var stageConfig = data.StageConfig;
|
||
if (stageConfig == null)
|
||
{
|
||
Debug.LogError($"[EventSlap] Stage config not found for ID: {data.CurrentStageID}");
|
||
return 0;
|
||
}
|
||
|
||
return GContext.container.Resolve<PlayerItemData>().IntInflation(stageConfig.StageHP, data.InflationRate);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 随机NPC数据
|
||
/// </summary>
|
||
private void RandomNpcData(Event1v1BattleSlapData data)
|
||
{
|
||
var robotList = data.Tables.TbRobot.DataList;
|
||
var index = Random.Range(0, robotList.Count);
|
||
var npcConfig = robotList[index];
|
||
while (npcConfig == null)
|
||
{
|
||
Debug.LogError($"npcConfig 配置错误 id {index}");
|
||
index = Random.Range(0, robotList.Count);
|
||
npcConfig = robotList[index];
|
||
}
|
||
|
||
data.NpcIcon = npcConfig.Avatar;
|
||
data.NpcName = npcConfig.Name;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Buff是否有效
|
||
/// </summary>
|
||
public bool BuffIsValid(Event1v1BattleSlapData data)
|
||
{
|
||
var center = GContext.container?.Resolve<BuffDataCenter>();
|
||
return center != null && center.GetWeelyBuffTimeData<EventSlapDamageBuff>() != null;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取Buff加成
|
||
/// </summary>
|
||
public float GetBuffBenefit(Event1v1BattleSlapData data)
|
||
{
|
||
if (!BuffIsValid(data))
|
||
return 0;
|
||
|
||
var buff = GContext.container?.Resolve<BuffDataCenter>()?.GetWeelyBuffTimeData<EventSlapDamageBuff>();
|
||
return buff?.Param ?? 0;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 从 <see cref="TbFishBuff"/> 中取扇耳光伤害加成(<see cref="EventSlapDamageBuff.Param"/>),命中第一条配置即可。
|
||
/// </summary>
|
||
private float GetEventSlapDamageBuffParamFromTable()
|
||
{
|
||
foreach (var fishBuff in Tables.TbFishBuff.DataList)
|
||
{
|
||
if (fishBuff.BuffParam is EventSlapDamageBuff slapDamageBuff)
|
||
return slapDamageBuff.Param;
|
||
}
|
||
|
||
return 0f;
|
||
}
|
||
|
||
#endregion
|
||
|
||
protected override void OnDataInitialized(IEventData data, FishingEvent eventConfig)
|
||
{
|
||
base.OnDataInitialized(data, eventConfig);
|
||
// 新一期活动(EventID 变化)时重置 Slap01,配合主面板 TriggerGuide(..., false) 实现每期仅可触发一次
|
||
GContext.container.Resolve<GuideDataCenter>().ClearGuide(GroupName.Slap01.ToString());
|
||
var slapData = (Event1v1BattleSlapData)data;
|
||
InitOnActive(slapData);
|
||
ShowFacePanel(slapData);
|
||
SaveData(slapData);
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 玩家攻击
|
||
|
||
/// <summary>
|
||
/// 玩家执行攻击
|
||
/// </summary>
|
||
public void ExecutePlayerAttack(int multiplier, int sourceIndex, Event1v1BattleSlapData data)
|
||
{
|
||
if (!data.IsActive || !data.IsOnGoing) return;
|
||
if (CheckAndHandleBattleTimeout(data, simulateNpcAttacks: false)) return;
|
||
|
||
_presentationData.Clear();
|
||
_presentationData.Multiplier = multiplier;
|
||
var damage = CalculateDamageBySource(data, multiplier, sourceIndex, out _presentationData.CanQuality);
|
||
var qualityForAgg = _presentationData.CanQuality;
|
||
damage = (int)(damage * (1 + GetBuffBenefit(data)));
|
||
Agg_event_slap(GetSpinachSourceAggType(sourceIndex), multiplier, qualityForAgg, data.PlayerHp,
|
||
BuffIsValid(data), data.NpcHp);
|
||
_presentationData.IsPlayAttacking = true;
|
||
_presentationData.SyncFromRealData(data);
|
||
|
||
data.NpcHp -= damage;
|
||
_presentationData.NpcHpAfterPlayerAttack = data.NpcHp;
|
||
|
||
if (data.NpcHp <= 0)
|
||
{
|
||
_presentationData.KilledNpc = true;
|
||
_presentationData.NextStageId = data.IsFinalStage ? data.InitConfig.InitialStage : data.StageConfig.NextStage;
|
||
HandleStageHpDepleted(data);
|
||
}
|
||
else
|
||
{
|
||
CheckAndSimulatePendingAttacks(data, false);
|
||
}
|
||
|
||
SaveData(data);
|
||
ShowFaceTv(data);
|
||
}
|
||
|
||
private void ShowFaceTv(Event1v1BattleSlapData data)
|
||
{
|
||
if (data == null || !data.IsActive) return;
|
||
if (string.IsNullOrEmpty(data.InitConfig?.TvPopUpPanel)) return;
|
||
UITypes.Event1v1BattleSlapTvPopupPanel.SetType(data.InitConfig.TvPopUpPanel);
|
||
GContext.container.Resolve<IFaceUIService>().AddPostFaceUI(UITypes.Event1v1BattleSlapTvPopupPanel);
|
||
}
|
||
|
||
#region 伤害计算
|
||
|
||
/// <summary>
|
||
/// 用Weight随机选择倍率
|
||
/// </summary>
|
||
private int SelectDamageByWeight(Event1v1BattleSlapData data)
|
||
{
|
||
var stageConfig = data.StageConfig;
|
||
var damageList = stageConfig.DamageList;
|
||
var weightList = stageConfig.Weight;
|
||
|
||
if (damageList == null || damageList.Count == 0)
|
||
return 1;
|
||
|
||
if (weightList == null || weightList.Count != damageList.Count)
|
||
return damageList[0];
|
||
|
||
int totalWeight = weightList.Sum();
|
||
if (totalWeight <= 0)
|
||
return damageList[0];
|
||
|
||
int randomValue = Random.Range(0, totalWeight);
|
||
int currentWeight = 0;
|
||
|
||
for (int i = 0; i < damageList.Count; i++)
|
||
{
|
||
currentWeight += weightList[i];
|
||
if (randomValue < currentWeight)
|
||
return damageList[i];
|
||
}
|
||
|
||
return damageList[0];
|
||
}
|
||
|
||
/// <summary>
|
||
/// 根据来源计算最终伤害
|
||
/// </summary>
|
||
public int CalculateDamageBySource(Event1v1BattleSlapData data, int multiplier, int sourceIndex, out int quality)
|
||
{
|
||
var spinachID = Tables.TbEventSlapMag.GetSpinachIDFromMag(multiplier);
|
||
var config = data.GetSpinachConfig(spinachID);
|
||
quality = config?.Quality ?? 0;
|
||
switch (sourceIndex)
|
||
{
|
||
case SOURCE_FISHING:
|
||
return CalculateFishingDamage(config, multiplier);
|
||
case SOURCE_BOMB:
|
||
return CalculateBombDamage(multiplier);
|
||
case SOURCE_SHOOT:
|
||
return CalculateRaidDamage(multiplier);
|
||
default:
|
||
return 0;
|
||
}
|
||
}
|
||
|
||
private int CalculateFishingDamage(EventSlapSpinach config,int multiplier)
|
||
{
|
||
return multiplier;
|
||
}
|
||
|
||
private int CalculateBombDamage(int multiplier)
|
||
{
|
||
return multiplier * Tables.TbGlobalConfig.EventSlapBombPoint;
|
||
}
|
||
|
||
private int CalculateRaidDamage(int multiplier)
|
||
{
|
||
return multiplier * Tables.TbGlobalConfig.EventSlapRaidPoint;
|
||
}
|
||
|
||
#endregion
|
||
|
||
#endregion
|
||
|
||
#region NPC攻击
|
||
|
||
/// <summary>
|
||
/// 模拟单次NPC攻击(不修改真实数据,用于判断是否致命)
|
||
/// </summary>
|
||
public bool SimulateStageAttack(Event1v1BattleSlapData data, int currentHp, out int damage, out int attackQuality,
|
||
out int attackMultiplier, out bool npcBuffApplied)
|
||
{
|
||
damage = CalculateStageDamage(data, out attackQuality, out attackMultiplier, out npcBuffApplied);
|
||
int newHp = currentHp - damage;
|
||
return newHp <= 0;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 计算NPC对玩家的伤害(完整流程)
|
||
/// </summary>
|
||
public int CalculateStageDamage(Event1v1BattleSlapData data, out int attackQuality, out int attackMultiplier,
|
||
out bool npcBuffApplied)
|
||
{
|
||
attackMultiplier = 0;
|
||
npcBuffApplied = false;
|
||
var stageConfig = data.StageConfig;
|
||
if (stageConfig == null)
|
||
{
|
||
Debug.LogError($"[EventSlap] Stage config not found for ID: {data.CurrentStageID}");
|
||
attackQuality = 0;
|
||
return -1;
|
||
}
|
||
|
||
attackMultiplier = SelectDamageByWeight(data);
|
||
int finalDamage = CalculateDamageBySource(data, attackMultiplier, SOURCE_FISHING, out attackQuality);
|
||
|
||
float coverage = Mathf.Clamp01(stageConfig.BuffCoverage);
|
||
if (finalDamage > 0 && coverage > 0f && Random.value < coverage)
|
||
{
|
||
float buffParam = GetEventSlapDamageBuffParamFromTable();
|
||
if (buffParam > 0f)
|
||
{
|
||
npcBuffApplied = true;
|
||
finalDamage = Mathf.Max(1, Mathf.RoundToInt(finalDamage * (1f + buffParam)));
|
||
}
|
||
}
|
||
|
||
#if UNITY_EDITOR
|
||
Debug.Log($"[EventSlap] Stage Damage: multiplier={attackMultiplier}, final={finalDamage}, quality={attackQuality}, buffCoverage={stageConfig.BuffCoverage}");
|
||
#endif
|
||
return finalDamage;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 检查并结算所有已到时的 NPC 攻击(<c>while (now >= NextAttackTime)</c> 可在一帧内连续多段)。
|
||
/// TV 只播一次敌方扇耳光:表现层仅展示最后一击——
|
||
/// <see cref="Event1v1BattleSlapPresentationData.PlayerHp"/> 为最后一击前血量,
|
||
/// <see cref="Event1v1BattleSlapPresentationData.AttackDamage"/> 为最后一击配置伤害;
|
||
/// 飘字/血条动画用表现前血量与真实结算后 <see cref="Event1v1BattleSlapData.PlayerHp"/> 的差值,故未死时等价于只展示最后一击的扣血(例:20 血连挨 5、8,表现为从 15 扣 8)。
|
||
/// </summary>
|
||
public bool CheckAndSimulatePendingAttacks(Event1v1BattleSlapData data, bool needClear = true)
|
||
{
|
||
var nowSeconds = new DateTimeOffset(ZZTimeHelper.UtcNow()).ToUnixTimeSeconds();
|
||
if (!data.IsActive || !data.IsOnGoing)
|
||
return false;
|
||
// 超时由 HandleBattleTimeout 内 ShowFaceTv 入待播;此处勿返回 true,否则 OnHomeStart 会再走立刻弹 TV 重复一次
|
||
if (CheckAndHandleBattleTimeout(data))
|
||
return false;
|
||
|
||
if (nowSeconds < data.NextAttackTime)
|
||
return false;
|
||
|
||
if (needClear)
|
||
{
|
||
_presentationData.Clear();
|
||
_presentationData.SyncFromRealData(data);
|
||
}
|
||
|
||
int lastDamage = 0;
|
||
int lastAttackQuality = 0;
|
||
int hpBeforeLastAttack = data.PlayerHp;
|
||
bool hasLethalAttack = false;
|
||
|
||
while (nowSeconds >= data.NextAttackTime)
|
||
{
|
||
hpBeforeLastAttack = data.PlayerHp;
|
||
bool isLethal = SimulateStageAttack(data, data.PlayerHp, out int damage, out int attackQuality,
|
||
out int attackMultiplier, out bool npcBuffApplied);
|
||
lastDamage = damage;
|
||
lastAttackQuality = attackQuality;
|
||
PresentationData.IsNpcBuffValid = npcBuffApplied;
|
||
if (damage >= 0)
|
||
Agg_event_slap_attacked(data.CurrentStageID, data.CurrentRoundForm1, attackMultiplier, npcBuffApplied,
|
||
isLethal);
|
||
|
||
if (isLethal)
|
||
{
|
||
hasLethalAttack = true;
|
||
data.PlayerHp = 0;
|
||
break;
|
||
}
|
||
|
||
data.PlayerHp -= damage;
|
||
int intervalSeconds = data.GetNextAttackInterval();
|
||
data.NextAttackTime += intervalSeconds;
|
||
}
|
||
|
||
if (!hasLethalAttack && nowSeconds >= data.NextAttackTime)
|
||
{
|
||
RefreshNextAttackTime(data);
|
||
}
|
||
|
||
_presentationData.IsNpcAttacking = true;
|
||
_presentationData.AttackDamage = lastDamage;
|
||
_presentationData.NpcAttackCanQuality = lastAttackQuality;
|
||
_presentationData.PlayerHp = hpBeforeLastAttack;
|
||
|
||
if (data.PlayerHp <= 0)
|
||
{
|
||
_presentationData.KilledPlayer = true;
|
||
data.PlayerHp = 0;
|
||
HandlePlayerHpDepleted(data);
|
||
}
|
||
|
||
SaveData(data);
|
||
return true;
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 菠菜系统
|
||
|
||
/// <summary>
|
||
/// 钓到菠菜后直接攻击NPC
|
||
/// </summary>
|
||
public void OnSpinachCaught(Event1v1BattleSlapData data)
|
||
{
|
||
if (!data.IsActive || !data.IsOnGoing) return;
|
||
var mag = GContext.container.Resolve<PlayerData>().GetMagnification();
|
||
ExecutePlayerAttack(mag, SOURCE_FISHING, data);
|
||
}
|
||
|
||
public void OnSpinachFromBomb(int mag, Event1v1BattleSlapData data)
|
||
{
|
||
if (!data.IsActive || !data.IsOnGoing) return;
|
||
ExecutePlayerAttack(mag, SOURCE_BOMB, data);
|
||
}
|
||
|
||
public void OnSpinachFromShoot(Event1v1BattleSlapData data, int mag)
|
||
{
|
||
if (!data.IsActive || !data.IsOnGoing) return;
|
||
ExecutePlayerAttack(mag, SOURCE_SHOOT, data);
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 关卡管理
|
||
|
||
public void StartBattle(Event1v1BattleSlapData data)
|
||
{
|
||
data.IsOnGoing = true;
|
||
data.BattleStartTime = new DateTimeOffset(ZZTimeHelper.UtcNow()).ToUnixTimeSeconds();
|
||
RefreshNextAttackTime(data);
|
||
SaveData(data);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 刷新下次NPC攻击时间
|
||
/// </summary>
|
||
public void RefreshNextAttackTime(Event1v1BattleSlapData data)
|
||
{
|
||
var now = ZZTimeHelper.UtcNow();
|
||
int intervalSeconds = data.GetNextAttackInterval();
|
||
Debug.Log($" slap NPC攻击时间刷新 {now} + {intervalSeconds}秒");
|
||
data.NextAttackTime = new DateTimeOffset(now).ToUnixTimeSeconds() + intervalSeconds;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 处理玩家HP归零
|
||
/// </summary>
|
||
public void HandlePlayerHpDepleted(Event1v1BattleSlapData data)
|
||
{
|
||
data.NextAttackTime = 0;
|
||
data.PlayerLifeCount--;
|
||
data.IsOnGoing = false;
|
||
data.BattleStartTime = 0;
|
||
if (data.HasLifeRemaining)
|
||
{
|
||
InitializeStageHp(data);
|
||
}
|
||
else if (TryEnterPaidRevivePendingState(data))
|
||
{
|
||
// 三命耗尽:有配置则停留当前进度,主界面下方走付费复活
|
||
}
|
||
else if (data.ShouldStartNewRound())
|
||
{
|
||
HandleStartNewRound(data);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 当前付费复活应使用的 TbIAPItemList 配置 id(来自 GlobalConfig.EventSlapIapList,下标封顶为最后一项)
|
||
/// </summary>
|
||
public int GetPaidReviveIapTableId(Event1v1BattleSlapData data)
|
||
{
|
||
var list = data.Tables.TbGlobalConfig.EventSlapIapList;
|
||
if (list == null || list.Count == 0)
|
||
return 0;
|
||
int idx = Math.Min(data.SlapPaidReviveCount, list.Count - 1);
|
||
return list[idx];
|
||
}
|
||
|
||
/// <summary>
|
||
/// 三命为 0、活动仍有效、且配置了可用 IAP 时,等待玩家付费复活(不自动开新轮)
|
||
/// </summary>
|
||
public bool IsAwaitingPaidRevive(Event1v1BattleSlapData data)
|
||
{
|
||
if (data == null || !data.IsActive || data.PlayerLifeCount > 0)
|
||
{
|
||
return false;
|
||
}
|
||
var iapId = GetPaidReviveIapTableId(data);
|
||
if (iapId <= 0)
|
||
{
|
||
Debug.LogError($"[EventSlap] IsAwaitingPaidRevive, return false");
|
||
return false;
|
||
}
|
||
var iapItem = data.Tables.TbIAPItemList?.GetOrDefault(iapId);
|
||
if (iapItem == null)
|
||
{
|
||
Debug.LogError($"[EventSlap] IsAwaitingPaidRevive, iapItem not found");
|
||
return false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
bool TryEnterPaidRevivePendingState(Event1v1BattleSlapData data)
|
||
{
|
||
if (!data.IsActive)
|
||
return false;
|
||
var iapId = GetPaidReviveIapTableId(data);
|
||
if (iapId <= 0)
|
||
return false;
|
||
if (data.Tables.TbIAPItemList?.GetOrDefault(iapId) == null)
|
||
{
|
||
Debug.LogError($"[EventSlap] EventSlapIapList id {iapId} not found in TbIAPItemList, fallback to new round.");
|
||
return false;
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 付费成功后:满血(当前关 MaxHp)并恢复 3 条命,保持关卡进度
|
||
/// </summary>
|
||
private void ApplyPaidRevive(Event1v1BattleSlapData data)
|
||
{
|
||
PresentationData.Clear();
|
||
data.SlapPaidReviveCount++;
|
||
data.PlayerLifeCount = 3;
|
||
InitializeStageHp(data);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 发起付费复活(无额外掉落展示)
|
||
/// </summary>
|
||
public async Task<bool> TryPurchasePaidReviveAsync(Event1v1BattleSlapData data)
|
||
{
|
||
if (!IsAwaitingPaidRevive(data))
|
||
return false;
|
||
|
||
int iapTableId = GetPaidReviveIapTableId(data);
|
||
GContext.container.Resolve<PlayerItemData>().ResolveIapId(iapTableId, out var iapItem, out _);
|
||
if (iapItem == null)
|
||
return false;
|
||
|
||
var emptyGift = new List<ItemData>();
|
||
var buyType = new ShopBuyTypeData
|
||
{
|
||
type = ShopBuyType.None,
|
||
ID = data.EventID,
|
||
IsHide = true,
|
||
NoShow = true,
|
||
extraPurcheData = new JObject
|
||
{
|
||
["event_slap_paid_revive"] = true,
|
||
["slap_paid_revive_index"] = data.SlapPaidReviveCount
|
||
}
|
||
};
|
||
|
||
bool ok = await GContext.container.Resolve<PlayerShopData>()
|
||
.OnBuy(0, buyType, iapItem, emptyGift);
|
||
if (ok)
|
||
{
|
||
ApplyPaidRevive(data);
|
||
SaveData(data);
|
||
}
|
||
|
||
return ok;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 处理关卡HP归零
|
||
/// </summary>
|
||
public void HandleStageHpDepleted(Event1v1BattleSlapData data)
|
||
{
|
||
Agg_event_slap_stage(data.CurrentRoundForm1, data.CurrentStageID);
|
||
data.needSendRewardStageID = data.CurrentStageID;
|
||
data.StageHistory.Add(data.CurrentStageID, true);
|
||
data.IsOnGoing = false;
|
||
if (data.IsFinalStage)
|
||
HandleStartNewRound(data);
|
||
else
|
||
ProceedToNextStage(data);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 进入下一关
|
||
/// </summary>
|
||
public void ProceedToNextStage(Event1v1BattleSlapData data)
|
||
{
|
||
data.CurrentStageID = data.GetNextStageID();
|
||
InitializeStageHp(data);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 开始新轮次
|
||
/// </summary>
|
||
public void HandleStartNewRound(Event1v1BattleSlapData data)
|
||
{
|
||
data.CurrentRoundForm1++;
|
||
InitRound(data);
|
||
Debug.Log($"[EventSlap] Starting new round: {data.CurrentRoundForm1}");
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 倒计时超时
|
||
|
||
/// <summary>
|
||
/// 检查战斗是否已超时(30分钟倒计时结束)
|
||
/// </summary>
|
||
public bool IsBattleTimeout(Event1v1BattleSlapData data)
|
||
{
|
||
if (!data.IsOnGoing || data.BattleStartTime <= 0)
|
||
return false;
|
||
|
||
var nowSeconds = new DateTimeOffset(ZZTimeHelper.UtcNow()).ToUnixTimeSeconds();
|
||
return nowSeconds - data.BattleStartTime >= Tables.TbGlobalConfig.EventBattleDuration*60;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 处理战斗超时。<paramref name="simulateNpcAttacks"/> 为 true 时先模拟超时期间 NPC 待定攻击,
|
||
/// 若能打死玩家则按 NPC 攻击致死结算;否则(或 <paramref name="simulateNpcAttacks"/> 为 false 时)直接判超时负。
|
||
/// </summary>
|
||
public void HandleBattleTimeout(Event1v1BattleSlapData data, bool simulateNpcAttacks = true)
|
||
{
|
||
if (!data.IsOnGoing) return;
|
||
|
||
if (simulateNpcAttacks)
|
||
{
|
||
Debug.Log($"[EventSlap] Battle timeout! 30min elapsed, simulating NPC attacks during timeout period...");
|
||
|
||
long battleEndTime = data.BattleStartTime + Tables.TbGlobalConfig.EventBattleDuration * 60;
|
||
|
||
if (SimulateNpcAttacksDuringTimeout(data, battleEndTime))
|
||
return;
|
||
}
|
||
|
||
Debug.Log($"[EventSlap] Battle timeout, settling as timeout defeat.");
|
||
|
||
Agg_event_slap_timeout(data.PlayerHp, data.NpcHp, data.CurrentRoundForm1, data.CurrentStageID);
|
||
|
||
_presentationData.Clear();
|
||
_presentationData.SyncFromRealData(data);
|
||
_presentationData.IsTimeoutDefeat = true;
|
||
_presentationData.KilledPlayer = true;
|
||
|
||
data.PlayerHp = 0;
|
||
HandlePlayerHpDepleted(data);
|
||
SaveData(data);
|
||
ShowFaceTv(data);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 模拟超时期间 NPC 的攻击:从 <see cref="Event1v1BattleSlapData.NextAttackTime"/> 到 battleEndTime 逐次模拟。
|
||
/// 若 NPC 在此期间打死玩家,填充表现数据为 NPC 攻击致死(TV 播 NPC 攻击动画),并完成结算。
|
||
/// </summary>
|
||
/// <returns>true 表示 NPC 在超时期间打死了玩家,已完成结算;false 表示玩家存活,调用方应走超时判负</returns>
|
||
private bool SimulateNpcAttacksDuringTimeout(Event1v1BattleSlapData data, long battleEndTime)
|
||
{
|
||
if (data.NextAttackTime <= 0 || data.NextAttackTime > battleEndTime)
|
||
return false;
|
||
|
||
_presentationData.Clear();
|
||
_presentationData.SyncFromRealData(data);
|
||
|
||
int lastDamage = 0;
|
||
int lastAttackQuality = 0;
|
||
int hpBeforeLastAttack = data.PlayerHp;
|
||
long simTime = data.NextAttackTime;
|
||
|
||
while (simTime <= battleEndTime)
|
||
{
|
||
hpBeforeLastAttack = data.PlayerHp;
|
||
bool isLethal = SimulateStageAttack(data, data.PlayerHp, out int damage, out int attackQuality,
|
||
out int attackMultiplier, out bool npcBuffApplied);
|
||
lastDamage = damage;
|
||
lastAttackQuality = attackQuality;
|
||
_presentationData.IsNpcBuffValid = npcBuffApplied;
|
||
if (damage >= 0)
|
||
Agg_event_slap_attacked(data.CurrentStageID, data.CurrentRoundForm1, attackMultiplier,
|
||
npcBuffApplied, isLethal);
|
||
|
||
if (isLethal)
|
||
{
|
||
Debug.Log($"[EventSlap] NPC killed player during timeout period at simTime={simTime}.");
|
||
data.PlayerHp = 0;
|
||
|
||
_presentationData.IsNpcAttacking = true;
|
||
_presentationData.AttackDamage = lastDamage;
|
||
_presentationData.NpcAttackCanQuality = lastAttackQuality;
|
||
_presentationData.PlayerHp = hpBeforeLastAttack;
|
||
_presentationData.KilledPlayer = true;
|
||
|
||
HandlePlayerHpDepleted(data);
|
||
SaveData(data);
|
||
ShowFaceTv(data);
|
||
return true;
|
||
}
|
||
|
||
data.PlayerHp -= damage;
|
||
int intervalSeconds = data.GetNextAttackInterval();
|
||
simTime += intervalSeconds;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 在各业务入口调用,检查超时并自动处理
|
||
/// </summary>
|
||
/// <returns>true 表示已触发超时处理,调用方应终止后续逻辑</returns>
|
||
public bool CheckAndHandleBattleTimeout(Event1v1BattleSlapData data, bool simulateNpcAttacks = true)
|
||
{
|
||
if (!IsBattleTimeout(data)) return false;
|
||
HandleBattleTimeout(data, simulateNpcAttacks);
|
||
return true;
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 奖励
|
||
|
||
public void SendStageReward(Event1v1BattleSlapData data)
|
||
{
|
||
var change = new ChangeSource(data.EventID, "Event_Minigame4", "Event_Minigame4_1V1Slap", "StageReward");
|
||
var config = Tables.TbEventSlapStage.GetOrDefault(data.needSendRewardStageID);
|
||
GContext.container.Resolve<PlayerItemData>()
|
||
.AddItemByDropLureInflation(data.InflationRate, config.DropId, change);
|
||
GContext.Publish(new ShowData());
|
||
data.needSendRewardStageID = 0;
|
||
SetRedPoint(data);
|
||
SaveData(data);
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 主面板
|
||
|
||
public async Task ShowSlapPanel(Event1v1BattleSlapData data = null)
|
||
{
|
||
try
|
||
{
|
||
if (data == null)
|
||
data = LoadData();
|
||
var panel = await GetBattleSlapContainer(data);
|
||
panel.SetData(this).ShowBattleSlapPanel();
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
Debug.LogError(e);
|
||
}
|
||
}
|
||
|
||
private async Task<Event1v1BattleSlapContainerPanel> GetBattleSlapContainer(Event1v1BattleSlapData data)
|
||
{
|
||
if (data?.InitConfig == null) return null;
|
||
UITypes.Event1v1BattleSlapPanel.SetType(data.InitConfig.UIPanel);
|
||
GameObject obj;
|
||
if (!UIManager.Instance.ContainsUI(UITypes.Event1v1BattleSlapPanel))
|
||
obj = await UIManager.Instance.ShowUILoad(UITypes.Event1v1BattleSlapPanel);
|
||
else
|
||
obj = UIManager.Instance.GetContainedUI(UITypes.Event1v1BattleSlapPanel);
|
||
var panel = obj.GetComponent<Event1v1BattleSlapContainerPanel>();
|
||
if (panel) return panel;
|
||
Debug.LogError("[EventSlap] Failed to get Event1v1BattleSlapPanel");
|
||
return null;
|
||
}
|
||
|
||
public async Task ShowSlapMatchPanelAndRandom(Event1v1BattleSlapData data)
|
||
{
|
||
try
|
||
{
|
||
var panel = await GetBattleSlapContainer(data);
|
||
panel.SetData(this).PlayRandomRpcAni();
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
Debug.LogError(e);
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 弹窗面板
|
||
|
||
public async Task<Event1v1BattleSlapTvPopupPanel> ShowTvPanel(Event1v1BattleSlapData data)
|
||
{
|
||
try
|
||
{
|
||
if (data?.InitConfig == null) return null;
|
||
UITypes.Event1v1BattleSlapTvPopupPanel.SetType(data.InitConfig.TvPopUpPanel);
|
||
GameObject obj = await UIManager.Instance.ShowUILoad(UITypes.Event1v1BattleSlapTvPopupPanel);
|
||
return obj.GetComponent<Event1v1BattleSlapTvPopupPanel>();
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
Debug.LogError(e);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
public async void ShowLoadingPanel(Event1v1BattleSlapData data)
|
||
{
|
||
try
|
||
{
|
||
if (data?.InitConfig == null) return;
|
||
UITypes.Event1v1BattleSlapLoadingPanel.SetType(data.InitConfig.LoadingPanel);
|
||
var obj = await UIManager.Instance.ShowUILoad(UITypes.Event1v1BattleSlapLoadingPanel);
|
||
obj.GetComponent<Event1v1BattleSlapLoadingPanel>()?.SetData(this, data);
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
Debug.LogError(e);
|
||
}
|
||
}
|
||
|
||
public async void ShowStagePanel(Event1v1BattleSlapData data)
|
||
{
|
||
try
|
||
{
|
||
if (data?.InitConfig == null) return;
|
||
UITypes.Event1v1BattleSlapPreviewPopupPanel.SetType(data.InitConfig.EventPreviewPanel);
|
||
await UIManager.Instance.ShowUILoad(UITypes.Event1v1BattleSlapPreviewPopupPanel);
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
Debug.LogError(e);
|
||
}
|
||
}
|
||
|
||
public async void ShowAllStageRewardPanel(Event1v1BattleSlapData data)
|
||
{
|
||
try
|
||
{
|
||
if (data?.InitConfig == null) return;
|
||
UITypes.Event1v1BattleSlapRewardPopupPanel.SetType(data.InitConfig.RewardPanel);
|
||
var panelObj = await UIManager.Instance.ShowUILoad(UITypes.Event1v1BattleSlapRewardPopupPanel);
|
||
var panel = panelObj.GetComponent<Event1v1BattleSlapRewardPopupPanel>();
|
||
panel.SetData(data);
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
Debug.LogError(e);
|
||
}
|
||
}
|
||
|
||
public void ShowFacePanel(Event1v1BattleSlapData data)
|
||
{
|
||
if (data == null) return;
|
||
UITypes.Event1v1BattleSlapFacePopupPanel.SetType(data.InitConfig.StageReviewPanel);
|
||
GContext.container.Resolve<IFaceUIService>()
|
||
.AddGiftFaceUI(data.EventID, UITypes.Event1v1BattleSlapFacePopupPanel, 0);
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 礼包
|
||
|
||
public async void ShowFestPackPanel()
|
||
{
|
||
try
|
||
{
|
||
var data = LoadData();
|
||
if (data?.InitConfig == null) return;
|
||
var packConfig = Tables.TbEventPackManager.GetOrDefault(data.InitConfig.PackID);
|
||
if (packConfig == null)
|
||
{
|
||
Debug.LogError($"[EventSlap] PackID {data.InitConfig.PackID} not found");
|
||
return;
|
||
}
|
||
var panelObj = await UIManager.Instance.ShowUILoad(new UIType(data.InitConfig.PackPanel));
|
||
var panel = panelObj.GetComponent<GeneralEventNormalAndFreePackPanel>();
|
||
panel.ShowUI(packConfig, data.EventID, data.IsClaimedFreePack, data.RemainingTime,
|
||
() =>
|
||
{
|
||
data.IsClaimedFreePack = true;
|
||
SetRedPoint(data);
|
||
SaveData(data);
|
||
}, null);
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
Debug.LogError(e);
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 红点
|
||
|
||
private string GetRedPointKey()
|
||
{
|
||
return "eventBattleSlap.red";
|
||
}
|
||
|
||
private bool GetRedPointState(Event1v1BattleSlapData data)
|
||
{
|
||
if (!data.IsActive) return false;
|
||
return !data.IsClaimedFreePack || data.IsNeedSendReward;
|
||
}
|
||
|
||
public void SetRedPoint(Event1v1BattleSlapData data)
|
||
{
|
||
RedPointManager.Instance?.SetRedPointState(GetRedPointKey(), GetRedPointState(data));
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 外部接口
|
||
|
||
public int GetActivateItemId(Event1v1BattleSlapData data = null)
|
||
{
|
||
if (data == null)
|
||
data = LoadData();
|
||
if (!data.IsActive || !data.IsOnGoing) return 0;
|
||
float basicProb = Random.Range(0, 1f);
|
||
if (!(basicProb <= data.Tables.TbGlobalConfig.EventSlapProb)) return 0;
|
||
try
|
||
{
|
||
var mag = GContext.container.Resolve<PlayerData>().GetMagnification();
|
||
var spinachID = data.Tables.TbEventSlapMag.GetSpinachIDFromMag(mag);
|
||
return data.GetSpinachConfig(spinachID).FishItemID;
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
Debug.LogError($"GetActivateItemId Error: {e.Message}");
|
||
return 0;
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 接口实现
|
||
|
||
public List<TargetAddData> AddBombRaidProgress(int multiple)
|
||
{
|
||
var data = LoadData();
|
||
if(!data.IsActive || !data.IsOnGoing) return null;
|
||
OnSpinachFromBomb(multiple, data);
|
||
var config = data.GetSpinachConfig(Tables.TbEventSlapMag.GetSpinachIDFromMag(multiple));
|
||
return config == null ? null : new List<TargetAddData> { new TargetAddData(config.FishItemID, 0, multiple) };
|
||
}
|
||
|
||
public List<TargetAddData> AddFishRaidProgress(int multiple)
|
||
{
|
||
var data = LoadData();
|
||
if(!data.IsActive || !data.IsOnGoing) return null;
|
||
OnSpinachFromShoot(data, multiple);
|
||
var config = data.GetSpinachConfig(Tables.TbEventSlapMag.GetSpinachIDFromMag(multiple));
|
||
return config == null ? null : new List<TargetAddData> { new TargetAddData(config.FishItemID, 0, multiple) };
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Agg
|
||
|
||
private static string GetSpinachSourceAggType(int sourceIndex)
|
||
{
|
||
return sourceIndex switch
|
||
{
|
||
SOURCE_BOMB => "bomb",
|
||
SOURCE_SHOOT => "raid",
|
||
SOURCE_FISHING => "fish",
|
||
_ => "unknown"
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// event_slap 获得辣椒罐头(发起攻击)时上报
|
||
/// </summary>
|
||
public void Agg_event_slap(string type, int multiple, int quality, int player_hp, bool is_buff, int enemy_hp)
|
||
{
|
||
#if AGG
|
||
using (var e = GEvent.GameEvent("event_slap"))
|
||
{
|
||
e.AddContent(nameof(type), type)
|
||
.AddContent(nameof(multiple), multiple)
|
||
.AddContent(nameof(quality), quality)
|
||
.AddContent(nameof(player_hp), player_hp)
|
||
.AddContent(nameof(is_buff), is_buff)
|
||
.AddContent(nameof(enemy_hp), enemy_hp);
|
||
}
|
||
#endif
|
||
}
|
||
|
||
/// <summary>
|
||
/// event_slap_stage 击败对手时上报
|
||
/// </summary>
|
||
public void Agg_event_slap_stage(int round, int stage_id)
|
||
{
|
||
#if AGG
|
||
using (var e = GEvent.GameEvent("event_slap_stage"))
|
||
{
|
||
e.AddContent(nameof(round), round)
|
||
.AddContent(nameof(stage_id), stage_id);
|
||
}
|
||
#endif
|
||
}
|
||
|
||
/// <summary>
|
||
/// event_slap_attacked 被对手攻击时上报
|
||
/// </summary>
|
||
public void Agg_event_slap_attacked(int stage_id, int round, int multiple, bool is_buff, bool is_defeat)
|
||
{
|
||
#if AGG
|
||
using (var e = GEvent.GameEvent("event_slap_attacked"))
|
||
{
|
||
e.AddContent(nameof(stage_id), stage_id)
|
||
.AddContent(nameof(round), round)
|
||
.AddContent(nameof(multiple), multiple)
|
||
.AddContent(nameof(is_buff), is_buff)
|
||
.AddContent(nameof(is_defeat), is_defeat);
|
||
}
|
||
#endif
|
||
}
|
||
|
||
/// <summary>
|
||
/// event_slap_timeout 战斗超时时上报
|
||
/// </summary>
|
||
public void Agg_event_slap_timeout(int player_hp, int enemy_hp, int round, int stage_id)
|
||
{
|
||
#if AGG
|
||
using (var e = GEvent.GameEvent("event_slap_timeout"))
|
||
{
|
||
e.AddContent(nameof(player_hp), player_hp)
|
||
.AddContent(nameof(enemy_hp), enemy_hp)
|
||
.AddContent(nameof(round), round)
|
||
.AddContent(nameof(stage_id), stage_id);
|
||
}
|
||
#endif
|
||
}
|
||
|
||
#endregion
|
||
|
||
#if UNITY_EDITOR
|
||
|
||
#region 调试
|
||
|
||
public void Debug_SetStage(int stageID)
|
||
{
|
||
var data = LoadData();
|
||
data.CurrentStageID = stageID;
|
||
data.StageHistory = new Dictionary<int, bool>();
|
||
var index = data.GetStageIndex(Tables.TbEventSlapStage.GetOrDefault(stageID));
|
||
for (int i = 0; i < index; i++)
|
||
{
|
||
data.StageHistory.Add(data.GetAllStage()[i].ID, true);
|
||
}
|
||
InitializeStageHp(data);
|
||
SaveData(data);
|
||
}
|
||
|
||
public void Debug_SetLife(int lifeCount)
|
||
{
|
||
var data = LoadData();
|
||
data.PlayerLifeCount = lifeCount;
|
||
SaveData(data);
|
||
}
|
||
|
||
#endregion
|
||
|
||
#endif
|
||
}
|
||
}
|