68 lines
4.3 KiB
C#
68 lines
4.3 KiB
C#
using Ananta.SDK.Network;
|
||
using Ananta.SDK.Serialization;
|
||
using Ananta.Server.ClientData.Client4229938;
|
||
using Ananta.Server.Protocol.Client4229938;
|
||
using SceneMethods = Ananta.Server.RpcTypes.Client4229938.Methods.GameScene;
|
||
|
||
namespace Ananta.Server.Handlers.Game;
|
||
|
||
/// <summary>
|
||
/// 敌人战斗姿态握手(2026-09-26 Wave 2,原生战斗管线复活)—— 移植 7k-v0.5
|
||
/// <c>GameRouter.EnemyAi.cs:21-49 SendEnemyCombatStanceAsync / ClearEnemyGunAim4229938</c>,
|
||
/// 逐行对齐,唯一差异:我们的 <c>TcpSession</c> 没有 <c>ClosedToken</c>,一律
|
||
/// <c>CancellationToken.None</c>(与 GameAdminBridge 既有风格一致)。
|
||
///
|
||
/// 这条握手是「客户端原生受击/战斗 locomotion」的激活钥匙之一:
|
||
/// <c>SyncBattleAnimSet</c> 让客户端把单位切到指定 AnimationSetConfig(空手 325/327、
|
||
/// 远程 337/338/340),原生 hurt/stagger 反应才有对应动画集可播;
|
||
/// <c>SyncAgentChangeWeapon</c> 负责卸/换武器(melee 空手 = weaponId 0)。
|
||
/// 7K 对 template 模式 / 无动画集可换的模板<b>整段跳过</b>,我们保持同样的判据,
|
||
/// 所以 spawnCombatMode=off / template 时本文件一个包都不会发。
|
||
/// </summary>
|
||
internal sealed partial class GameRouter
|
||
{
|
||
/// <summary>7K 原判据:melee 模式 + 已卸武器 + 有空手技能 = 空手单位。</summary>
|
||
private static bool IsUnarmedEnemyProfile4229938(EnemyAiProfile4229938? profile)
|
||
=> profile is { CombatMode: "melee", Weapon: 0, UnarmedAttacks.Count: > 0 };
|
||
|
||
/// <summary>
|
||
/// 清空上半身持枪瞄准(移植 7K <c>ClearEnemyGunAim4229938</c>,消费方注释原文保留)。
|
||
/// 两个包都是裸 u64(8B,无 marker)—— 7K 用 <c>new UxWriter().U64(id)</c>,照抄。
|
||
/// </summary>
|
||
private static async Task ClearEnemyGunAim4229938(TcpSession session, ulong id)
|
||
{
|
||
// Native 127C7F20 -> GunAimModule_Npc.Stop(pid): upper-body aim is
|
||
// independent of the weapon model and the selected battle locomotion set.
|
||
var payload = new UxWriter().U64(id).ToArray();
|
||
await session.NotifyAsync(MethodId.IGameSceneToClient_SyncEndShootingState, payload, CancellationToken.None);
|
||
// Native 127704B0 removes shooting tag 919, sends inward signal 1850,
|
||
// and exits the shooting skill transition when the state-tree is active.
|
||
await session.NotifyAsync(MethodId.IGameSceneToClient_SyncEndShootGameplayTag, payload, CancellationToken.None);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 战斗姿态握手(移植 7K <c>SendEnemyCombatStanceAsync</c>)。
|
||
/// 判据与 7K 完全一致:profile null → 跳过;非空手且 BattleAnimSetTag==0 → 跳过
|
||
/// (template / 持武器近战模板 7K 本来一个包都不发)。
|
||
/// </summary>
|
||
internal static async Task SendEnemyCombatStanceAsync(TcpSession session, ulong id, EnemyAiProfile4229938? profile)
|
||
{
|
||
if (profile is null) return;
|
||
var unarmed = IsUnarmedEnemyProfile4229938(profile);
|
||
if (!unarmed && profile.BattleAnimSetTag == 0) return;
|
||
if (unarmed) await ClearEnemyGunAim4229938(session, id);
|
||
// Weapon removal alone does not replace the battle movement action set.
|
||
await session.NotifyAsync(MethodId.SyncAgentChangeWeapon, UxSerializer.Serialize(
|
||
new SceneMethods.EnemyAiWeapon4229938 { agentId = id, weaponId = profile.Weapon }), CancellationToken.None);
|
||
// ApplyBattleAnimSetTag returns immediately for the same cached tag.
|
||
// During the bounded loading warmups, remove/re-add the override so it
|
||
// cannot remain a cached no-op after a late weapon/module initialization.
|
||
if (unarmed && profile.BattleAnimSetTag != 0)
|
||
await session.NotifyAsync(MethodId.IGameSceneToClient_SyncBattleAnimSet, UxSerializer.Serialize(
|
||
new SceneMethods.EnemyAiBattleAnim4229938 { id = id, animSetTag = 0 }), CancellationToken.None);
|
||
await session.NotifyAsync(MethodId.IGameSceneToClient_SyncBattleAnimSet, UxSerializer.Serialize(
|
||
new SceneMethods.EnemyAiBattleAnim4229938 { id = id, animSetTag = profile.BattleAnimSetTag }), CancellationToken.None);
|
||
session.Log.Info($"[ENEMY-STANCE] uid={id} mode={profile.CombatMode} unarmed={unarmed} weapon={profile.Weapon} animSet={profile.BattleAnimSetTag} attacks={string.Join(',', profile.Attacks)}");
|
||
}
|
||
}
|