454 lines
24 KiB
C#
454 lines
24 KiB
C#
using System.Collections.Concurrent;
|
||
using System.Runtime.CompilerServices;
|
||
using Ananta.SDK.Network;
|
||
using Ananta.SDK.Serialization;
|
||
using Auto = Ananta.Server.RpcTypes.Client4229938.Auto;
|
||
using Ananta.Server.Configuration;
|
||
using Ananta.Server.Protocol.Client4229938;
|
||
using Ananta.Server.RpcTypes.Client4229938;
|
||
using GameMethods = Ananta.Server.RpcTypes.Client4229938.Methods.Game;
|
||
using SceneMethods = Ananta.Server.RpcTypes.Client4229938.Methods.GameScene;
|
||
|
||
namespace Ananta.Server.Handlers.Game;
|
||
|
||
/// <summary>
|
||
/// 梅卡妮卡(Mechanika,spirit <b>15021017</b>)的召唤能力 —— <b>机械羊</b>。
|
||
///
|
||
/// <b>2026-09-27 17:2x 定稿(用户拍板):</b>
|
||
/// · 一次 E 在玩家<b>左前 / 右前 / 背后</b>各放一只(默认 3 只,旋钮可调)。
|
||
/// · <b>取消 10 米判定</b>:不再有"范围内已有羊就走念力"这条,想召就召。
|
||
/// · <b>取消数量上限</b>:不再有 hard cap(旋钮 <c>mechanikaSheepMaxAlive</c>,0 = 不限制)。
|
||
/// · <b>羊不会移动</b> —— 这是客户端结构决定的,不是 bug:
|
||
/// 模板 40120651 的 AI 40213004 行为树是 <c>Behavior_EmptyBehavior</c>(字面"无战斗AI"),
|
||
/// 服务端推的 <c>SyncEnemyBattleMoveTowardUnit</c> 全被忽略(实机 150+ 条,坐标 3 分钟零变化;
|
||
/// 客户端 <c>AskUnitMoveActionSimple</c> 仍在正常上报这只羊,说明它<b>在场</b>只是不动)。
|
||
/// 带真树(<c>MeikaShip</c>)的羊模板模型是透明的,客户端里也没有这棵树 ⇒ 换模板救不了。
|
||
/// ⇒ <b>跟随/移动相关代码已整块删除</b>,羊就是定点召唤物。
|
||
///
|
||
/// <b>取证(2026-09-27 读 <c>DRMK-PS_09_7k-v0.5</c> + 客户端配置表):</b>
|
||
/// · <c>SkillConfig 51937722</c>「梅卡定制召唤羊」SkillCastTypeTag=2。
|
||
/// · <c>SkillJumpConfig 97011851</c>:<c>return !IsHaveEnemyByDistance(40980002, 10);</c> → 跳到 51937722;
|
||
/// 另一条 <c>97011078</c><c>return true</c> → 跳到 51934941。
|
||
/// ⛑⛑ 但这两个是 <b>tag=2,不是可释放的根技能</b>,且没有任何 jump 指向它们、
|
||
/// 也没有 FightSkillConfig 行把 15021017 写进 SpiritId ⇒
|
||
/// <b>这版客户端里梅卡妮卡的羊没有任何原生 E 入口</b>(捐赠端的 E 绑定是他自己加的)。
|
||
/// 现做法:借 <b>51939201</b>(tag=6 的空壳占位技能)当 E 的壳子,服务端接住后召羊。
|
||
/// 详见 <c>GameplaySettings.MechanikaSheepActiveSkillId</c> 与硬规则 201。
|
||
///
|
||
/// <b>模板:</b><c>AgentConfig 40120651</c>「羊」GeneralModelId 86952761(<b>有模型</b>)、
|
||
/// AI 40213004、ShowHPBar=True(血条用 <c>UnitStateConfig.HideHp</c> 收掉)。
|
||
/// ⛔ 40980002 / 40120907「梅卡羊」模型 86952527 <b>在这版客户端里是透明的</b>,别再默认。
|
||
/// </summary>
|
||
internal sealed partial class GameRouter
|
||
{
|
||
private const uint MechanikaSpirit4229938 = 15_021_017;
|
||
private const uint MechanikaSummonSkill4229938 = 51_937_722; // 梅卡定制召唤羊
|
||
/// <summary>「梅卡定制念力」—— 51937721/51937722 的 <c>SkillReplace</c> 就挂在这个根技能上。</summary>
|
||
private const uint MechanikaMindRootSkill4229938 = 51_937_721;
|
||
/// <summary>「通用念力砸击连招01_特殊可破坏物」—— 原来 10 米内有羊时 <c>97011078</c> 跳到这里。</summary>
|
||
private const uint MechanikaMindStrikeSkill4229938 = 51_934_941;
|
||
/// <summary>
|
||
/// 「混厄召唤师召唤精灵1」—— <b>E 槽的壳子</b>。
|
||
/// ⛑⛑ 为什么是它:E 槽只认 <c>SkillCastTypeTag=6</c>。它 tag=6 且是<b>空壳占位技能</b>
|
||
/// (51939200–51939204 五个字段几乎全空、不召唤任何东西、只出现在 SkillConfig.json)
|
||
/// ⇒ 借它的壳发一次 AskUseSkill,<b>没有任何副作用</b>。实机已验证客户端肯发。
|
||
/// </summary>
|
||
private const uint MechanikaSheepCastProxySkill4229938 = 51_939_201;
|
||
/// <summary>「梅卡羊传送」—— 带 MeikaShip 树的模板唯一的技能(现用模板用不上,仅登记用)。</summary>
|
||
private const uint MechanikaSheepTeleportSkill4229938 = 51_984_701;
|
||
|
||
/// <summary>羊落点离玩家的距离(米)。沿用原来的 2.5 m,避免直接怼在玩家身上。</summary>
|
||
private const float MechanikaSheepDropMeters4229938 = 2.5f;
|
||
/// <summary>
|
||
/// 羊<b>落地时朝向</b>再转多少度。默认 <b>180</b>。
|
||
/// ⛑ 2026-09-27 17:3x 用户实机:按"落点方位角"直接给 facing 时羊全是<b>屁股对着玩家</b>
|
||
/// (落点在左前,羊也朝左前 = 背对站在中心的玩家)⇒ 统一再转 180° 让它们面朝玩家。
|
||
/// ⛑ 只改朝向,<b>不动落点</b>:落点仍由 <see cref="MechanikaSheepSpawnAngles"/> 决定。
|
||
/// </summary>
|
||
private const float MechanikaSheepFacingOffsetDeg4229938 = 180f;
|
||
|
||
private static readonly ConditionalWeakTable<TcpSession, MechanikaRuntime4229938> Mechanika4229938 = new();
|
||
|
||
private sealed class MechanikaRuntime4229938
|
||
{
|
||
internal readonly SemaphoreSlim Gate = new(1, 1);
|
||
internal readonly ConcurrentDictionary<ulong, MechanikaSheepState4229938> States = new();
|
||
internal ulong Owner;
|
||
internal int Generation;
|
||
}
|
||
|
||
private sealed class MechanikaSheepState4229938
|
||
{
|
||
internal long ActionUntil;
|
||
internal int SkillInstance;
|
||
}
|
||
|
||
private static bool MechanikaReady4229938(WorldEntryState? state)
|
||
=> state is { Ready: true, CombatProfilePublished: true, ActiveSpiritTemplateId: MechanikaSpirit4229938,
|
||
PendingSwitchTemplateId: 0, PendingTeleportId: 0 } && state.ActiveSpiritUnitId != 0;
|
||
|
||
/// <summary>
|
||
/// 梅卡妮卡的"羊技能族"白名单。⛑ 这四个 id <b>不在任何 FightSkillConfig 行里</b>,
|
||
/// 所以 <c>weapon.AllowsSkill</c> 一定判 false —— 必须在到达那条校验<b>之前</b>放行,
|
||
/// 否则客户端发来的 51937722 / 51934941 / 51939201 会被
|
||
/// <c>[COMBAT] reject unknown-skill</c> 吃掉。
|
||
/// </summary>
|
||
internal static bool AllowsMechanikaSheepSkill4229938(uint templateId, uint skillId)
|
||
=> templateId == MechanikaSpirit4229938 &&
|
||
(skillId == MechanikaMindRootSkill4229938 ||
|
||
skillId == MechanikaSummonSkill4229938 ||
|
||
skillId == MechanikaMindStrikeSkill4229938 ||
|
||
skillId == MechanikaSheepCastProxySkill4229938);
|
||
|
||
/// <summary>
|
||
/// 一次按键放一批机械羊(默认 3 只:左前 45° / 右前 −45° / 背后 180°)。
|
||
/// 调用方:<c>AskUseSkill(SkillId=51937722)</c>、<c>51937721</c>、<c>51939201</c>(当前 E 走的那个)
|
||
/// 与流派 <c>ActiveSkill</c> 兜底。
|
||
///
|
||
/// ⛑ 2026-09-27:<b>10 米判定与数量上限都已按用户要求删除</b> —— 按一次出一批,可以一直出。
|
||
/// 想加安全闸就配 <c>gameplay.mechanikaSheepMaxAlive</c>(0 = 不限制)。
|
||
/// </summary>
|
||
internal static async Task<(bool Ok, string Message)> SummonMechanikaSheep4229938(TcpSession session)
|
||
{
|
||
var state = GetStateIfExists(session);
|
||
if (!MechanikaReady4229938(state) || !state!.HasLastReportedPlayerTransform)
|
||
return (false, "请先切换到梅卡妮卡,进入场景并移动一下。");
|
||
|
||
var runtime = Mechanika4229938.GetValue(session, _ => new());
|
||
await runtime.Gate.WaitAsync(CancellationToken.None);
|
||
try
|
||
{
|
||
if (!MechanikaReady4229938(state)) return (false, "角色正在切换或传送。");
|
||
|
||
var gameplay = PrivateServerConfigStore.Current.Gameplay;
|
||
var maxAlive = gameplay.MechanikaSheepMaxAlive;
|
||
if (maxAlive > 0 && runtime.States.Count >= maxAlive)
|
||
return (true, $"机械羊数量已达上限 {maxAlive} 只(gameplay.mechanikaSheepMaxAlive;置 0 不限制)。");
|
||
|
||
runtime.Owner = state.ActiveSpiritUnitId;
|
||
runtime.Generation = state.WorldEntryControlGeneration;
|
||
var p = state.HasLastReportedPlayerTransform
|
||
? state.LastReportedPlayerPosition
|
||
: state.WorldEntryCreateHeroPosition;
|
||
if (!float.IsFinite(p.X + p.Y + p.Z)) return (false, "角色坐标无效。");
|
||
|
||
var facing = state.LastReportedPlayerRotation.Y;
|
||
if (!float.IsFinite(facing)) facing = 0f;
|
||
|
||
// 落点角度(度)。默认 [45, -45, 180] = 左前 / 右前 / 背后。
|
||
var angles = gameplay.MechanikaSheepSpawnAngles is { Length: > 0 } a ? a : new[] { 45, -45, 180 };
|
||
var perCast = gameplay.MechanikaSheepPerCast > 0 ? gameplay.MechanikaSheepPerCast : angles.Length;
|
||
var spawnPath = gameplay.MechanikaSheepSpawnPath;
|
||
var useHandwritten = string.Equals(spawnPath, "handwritten", StringComparison.OrdinalIgnoreCase);
|
||
|
||
var spawned = 0;
|
||
for (var i = 0; i < perCast; i++)
|
||
{
|
||
// 硬规则 181:facing 是<b>度数</b>,喂 sin/cos 前必须转弧度。
|
||
var offset = angles[i % angles.Length];
|
||
var yaw = (facing + offset) * MathF.PI / 180f;
|
||
var position = new Vec3(p.X + MathF.Sin(yaw) * MechanikaSheepDropMeters4229938,
|
||
p.Y,
|
||
p.Z + MathF.Cos(yaw) * MechanikaSheepDropMeters4229938);
|
||
// 朝向 = 落点方位角 + 180°(面朝玩家),并归一到 [0,360):玩家 facing 可能为负。
|
||
var sheepFacing = ((facing + offset + MechanikaSheepFacingOffsetDeg4229938) % 360f + 360f) % 360f;
|
||
|
||
ulong id;
|
||
if (useHandwritten)
|
||
{
|
||
id = GameAdminBridge.NextRetirableNpcUid();
|
||
}
|
||
else
|
||
{
|
||
var result = await GameAdminBridge.SpawnAgentAsync(
|
||
gameplay.MechanikaSheepAgentId, position.X, position.Y, position.Z, sheepFacing,
|
||
hostile: true, // AgentConfig 40120651 在 Hostile 桶里,校验要求 true
|
||
engage: false, // 羊不进敌人账本:它不是拿来打的
|
||
forceEnemyRelation: false, // 不能把玩家的羊标成玩家的敌人
|
||
campOverride: (byte)gameplay.MechanikaSheepCamp,
|
||
treeNameOverride: gameplay.MechanikaSheepTreeName,
|
||
occurOverride: gameplay.MechanikaSheepOccurId);
|
||
if (!result.Ok)
|
||
{
|
||
session.Log.Warn($"[MECHANIKA] spawn #{i} failed: {result.Message}");
|
||
continue;
|
||
}
|
||
id = result.Uid;
|
||
await ActivateMechanikaSheepAsync(session, id);
|
||
}
|
||
|
||
// ★ 血条:AgentConfig 40120651.ShowHPBar=True(客户端配置,服务端改不了),
|
||
// 唯一可写的开关是 UnitStateConfig.HideHp —— 机械狗就是靠它把血条收掉的
|
||
// (GameRouter.HackerSummon.cs)。详见 GameplaySettings.MechanikaSheepHideHpBar。
|
||
if (gameplay.MechanikaSheepHideHpBar)
|
||
await HideMechanikaSheepHpBarAsync(session, id);
|
||
|
||
runtime.States[id] = new MechanikaSheepState4229938();
|
||
|
||
if (useHandwritten)
|
||
{
|
||
try
|
||
{
|
||
await SendMechanikaSheepAgentAsync(session, id, runtime.Owner, position, sheepFacing);
|
||
}
|
||
catch
|
||
{
|
||
runtime.States.TryRemove(id, out _);
|
||
throw;
|
||
}
|
||
}
|
||
|
||
spawned++;
|
||
}
|
||
|
||
if (spawned == 0)
|
||
return (false, "机械羊生成失败(见上一行 [MECHANIKA] spawn 日志)。");
|
||
|
||
session.Log.Info($"[MECHANIKA] summoned sheep={spawned} template={gameplay.MechanikaSheepAgentId} "
|
||
+ $"path={spawnPath} owner={runtime.Owner} angles=[{string.Join(",", angles)}] "
|
||
+ $"alive={runtime.States.Count} at=({p.X:F2},{p.Y:F2},{p.Z:F2}) facing={facing:F2}");
|
||
return (true, $"已在身边放下 {spawned} 只机械羊(左前/右前/背后),场上共 {runtime.States.Count} 只。");
|
||
}
|
||
finally { runtime.Gate.Release(); }
|
||
}
|
||
|
||
/// <summary>
|
||
/// 生成之后的"激活批次"。<c>GameplaySettings.MechanikaSheepActivate</c>:
|
||
/// "battleAiS"(默认)只切战斗树、<b>不锁玩家</b>;"full" 走 GM 那套(会攻击玩家);"none" 不发。
|
||
/// ⛑ 1.5 s 后重发一次是照 <c>ReEngageSpawnedEnemyAsync</c> 的节奏 —— 第一批可能在客户端
|
||
/// 还没把这个单位初始化完就被吃掉了。
|
||
/// </summary>
|
||
private static async Task ActivateMechanikaSheepAsync(TcpSession session, ulong id)
|
||
{
|
||
var mode = PrivateServerConfigStore.Current.Gameplay.MechanikaSheepActivate;
|
||
if (string.Equals(mode, "none", StringComparison.OrdinalIgnoreCase))
|
||
return;
|
||
|
||
if (string.Equals(mode, "full", StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
var state = GetStateIfExists(session);
|
||
var playerUnit = state is not null && state.WorldEntryControlUnit != 0
|
||
? state.WorldEntryControlUnit
|
||
: state?.ActiveSpiritUnitId ?? 0;
|
||
await EngageSpawnedEnemyAsync(session, id, playerUnit);
|
||
_ = ReEngageSpawnedEnemyAsync(session, id);
|
||
session.Log.Info($"[MECHANIKA] activate uid={id} mode=full (GM 那套;羊会攻击玩家)");
|
||
return;
|
||
}
|
||
|
||
try
|
||
{
|
||
await session.NotifyAsync(MethodId.SyncBattleAiS,
|
||
UxSerializer.Serialize(new SceneMethods.SyncBattleAiS { agentId = id, value = true }),
|
||
CancellationToken.None);
|
||
await session.NotifyAsync(MethodId.SyncEnemyDetect,
|
||
UxSerializer.Serialize(new GameMethods.EnemyDetectData4229938 { Id = id, Stealth = 0 }),
|
||
CancellationToken.None);
|
||
session.Log.Info($"[MECHANIKA] activate uid={id} mode=battleAiS (切战斗树,不锁玩家)");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
session.Log.Warn($"[MECHANIKA] activate failed uid={id}: {ex.Message}");
|
||
return;
|
||
}
|
||
|
||
// 1.5 s 后补一次(对齐 ReEngageSpawnedEnemyAsync 的节奏)。
|
||
_ = Task.Run(async () =>
|
||
{
|
||
try
|
||
{
|
||
await Task.Delay(1500, CancellationToken.None);
|
||
if (!SessionAlive4229938(session)) return;
|
||
await session.NotifyAsync(MethodId.SyncBattleAiS,
|
||
UxSerializer.Serialize(new SceneMethods.SyncBattleAiS { agentId = id, value = true }),
|
||
CancellationToken.None);
|
||
session.Log.Info($"[MECHANIKA] re-activate uid={id} mode=battleAiS");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
session.Log.Warn($"[MECHANIKA] re-activate failed uid={id}: {ex.Message}");
|
||
}
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// 收掉羊头顶的血条(<c>SyncAddUnitState</c> + <c>UnitStateConfig.HideHp = 10266</c>)。
|
||
/// 与 <c>GameRouter.HackerSummon.cs</c> 给机械狗做的<b>完全同一招</b>。
|
||
/// ⛑ 1.5 s 后重发一次:spawn 批是几十个包,AddUnitState 可能比单位在客户端建好更早到,
|
||
/// 那就白发了(<c>ReassertHiddenHpBarAsync</c> / <c>ReEngageSpawnedEnemyAsync</c> 同样延 1.5 s)。
|
||
/// </summary>
|
||
private static async Task HideMechanikaSheepHpBarAsync(TcpSession session, ulong id)
|
||
{
|
||
async Task Send()
|
||
{
|
||
await session.NotifyAsync(MethodId.SyncAddUnitState,
|
||
UxSerializer.Serialize(new GameMethods.SyncAddUnitStateArgs4229938
|
||
{
|
||
Id = id,
|
||
State = LocalEnemyUnitStateHideHp,
|
||
EffectFreezeState = 0u,
|
||
}), CancellationToken.None);
|
||
}
|
||
|
||
try
|
||
{
|
||
await Send();
|
||
session.Log.Info($"[MECHANIKA] hide hp bar uid={id} state=HideHp({LocalEnemyUnitStateHideHp})");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
session.Log.Warn($"[MECHANIKA] hide hp bar failed uid={id}: {ex.Message}");
|
||
return;
|
||
}
|
||
|
||
_ = Task.Run(async () =>
|
||
{
|
||
try
|
||
{
|
||
await Task.Delay(1500, CancellationToken.None);
|
||
if (!SessionAlive4229938(session)) return;
|
||
await Send();
|
||
session.Log.Info($"[MECHANIKA] reassert hide hp bar uid={id}");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
session.Log.Warn($"[MECHANIKA] reassert hide hp bar failed uid={id}: {ex.Message}");
|
||
}
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// 刷一只羊(<c>mechanikaSheepSpawnPath="handwritten"</c> 时走这条)。
|
||
/// 序列照抄 <c>GameRouter.Civilians.cs</c> 的
|
||
/// <c>SendLeftBehindAgentAsync</c>(<c>SyncRaidBattleUnitAgent</c> 通道,实机验证过的那条)。
|
||
/// ⛑ 阵营:硬规则 132 —— 友方 NPC 一律不发 <c>SyncForceEnemyRelation</c>、不登记进敌人账本。
|
||
/// </summary>
|
||
private static async Task SendMechanikaSheepAgentAsync(TcpSession session, ulong id, ulong owner,
|
||
Vec3 position, float facing)
|
||
{
|
||
var gameplay = PrivateServerConfigStore.Current.Gameplay;
|
||
var ai = new SceneMethods.LinkAIAgentInfo { Uid = id, Nickname = "梅卡羊", Fashion = null };
|
||
var syncInfo = new SceneMethods.AgentSyncClientInfo
|
||
{
|
||
isApproachNpc = true,
|
||
TriggerLeaveEvent = true,
|
||
approachDistance = 300,
|
||
LeaveDistance = 600,
|
||
sitIndex = -1,
|
||
SpoonAgentId = -1,
|
||
treeName = string.IsNullOrWhiteSpace(gameplay.MechanikaSheepTreeName)
|
||
? "Main_Common"
|
||
: gameplay.MechanikaSheepTreeName,
|
||
AISetting = 40_213_004,
|
||
stimIDList = [],
|
||
spawnEffectId = [],
|
||
indoorList = [],
|
||
roomIds = [],
|
||
returnPoiActionIds = [],
|
||
AIAgentInfo = ai,
|
||
};
|
||
|
||
await session.NotifyAsync(MethodId.SyncLogicAgentEnter,
|
||
UxSerializer.Serialize(WorldCodec.LogicAgentEnter(id)), CancellationToken.None);
|
||
await session.NotifyAsync(MethodId.SyncManagedLogicAgent,
|
||
UxSerializer.Serialize(WorldCodec.ManagedLogicAgent(id, Profile.PlayerPid, 0)), CancellationToken.None);
|
||
await session.NotifyAsync(MethodId.SyncRaidBattleUnitAgent,
|
||
UxSerializer.Serialize(new SceneMethods.RaidBattleUnitAgent
|
||
{
|
||
SkillId = -1,
|
||
HSummonIndex = -1,
|
||
SpoonAgentId = -1,
|
||
FashionIdList = [],
|
||
VehicleIndex = -1,
|
||
IsBorn = true,
|
||
BattleAiS = true,
|
||
agentSyncClientInfo = syncInfo,
|
||
WeaponId = 98005506u,
|
||
SpawnType = 1,
|
||
AIAgentInfo = ai,
|
||
Id = id,
|
||
TemplateId = gameplay.MechanikaSheepAgentId,
|
||
Position = new Auto.UXVector3 { X = position.X, Y = position.Y, Z = position.Z },
|
||
FacingDirection = facing,
|
||
ManagedPid = Profile.PlayerPid,
|
||
GroundData = new Auto.MoveActionGroundData { LocalPos = new Auto.UXVector3() },
|
||
}), CancellationToken.None);
|
||
await session.NotifyAsync(MethodId.SyncUnitOccur,
|
||
UxSerializer.Serialize(new SceneMethods.SyncUnitOccur
|
||
{
|
||
enemyId = id,
|
||
occurId = gameplay.MechanikaSheepOccurId,
|
||
dullSecs = 0f,
|
||
position = new Auto.UXVector3 { X = position.X, Y = position.Y, Z = position.Z },
|
||
rotateY = facing,
|
||
}), CancellationToken.None);
|
||
await session.NotifyAsync(MethodId.SyncAgentCampInfo,
|
||
UxSerializer.Serialize(new SceneMethods.SyncAgentCampInfo { pid = id, camp = (byte)gameplay.MechanikaSheepCamp }), CancellationToken.None);
|
||
await session.NotifyAsync(MethodId.SyncAttachBattleModule,
|
||
UxSerializer.Serialize(new SceneMethods.SyncAttachBattleModule { unitId = id }), CancellationToken.None);
|
||
await session.NotifyAsync(MethodId.SyncUnitAttrs,
|
||
UxSerializer.Serialize(new SceneMethods.SyncUnitAttrs
|
||
{
|
||
unitId = id,
|
||
attrs = new Dictionary<uint, float> { [1] = 100f },
|
||
}), CancellationToken.None);
|
||
await session.NotifyAsync(MethodId.SyncUnitHp,
|
||
UxSerializer.Serialize(new SceneMethods.SyncUnitHp { unitId = id, hp = 100f }), CancellationToken.None);
|
||
await session.NotifyAsync(MethodId.IGameSceneToClient_SyncAgentForceGo,
|
||
UxSerializer.Serialize(new SceneMethods.SyncAgentForceGo4229938 { id = id, isForceGo = true }),
|
||
CancellationToken.None);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 羊自己放的原生技能(MeikaShip 只列了传送 51984701):只登记生命周期,
|
||
/// <b>绝扣不到玩家头上</b>,也绝不伪造伤害/命中。
|
||
/// ⛑ 现用的 40120651 行为树是 <c>Behavior_EmptyBehavior</c>,实际上不会放技能;
|
||
/// 这条留着是为了万一换成带树的模板时,不把羊的技能记到玩家连招账上。
|
||
/// </summary>
|
||
internal static async Task<bool> RecordMechanikaSkill4229938(TcpSession session, SceneMethods.SkillUseData request)
|
||
{
|
||
if (!Mechanika4229938.TryGetValue(session, out var runtime)) return false;
|
||
if (!runtime.States.TryGetValue(request.Releaser, out var sheep)
|
||
|| request.SkillId != MechanikaSheepTeleportSkill4229938) return false;
|
||
var state = GetStateIfExists(session);
|
||
if (!MechanikaReady4229938(state) || runtime.Generation != state!.WorldEntryControlGeneration
|
||
|| runtime.Owner != state.ActiveSpiritUnitId) return false;
|
||
|
||
sheep.SkillInstance = request.SkillInstanceId;
|
||
sheep.ActionUntil = Environment.TickCount64 + 5000;
|
||
session.Log.Info($"[MECHANIKA] native-skill entity={request.Releaser} skill={request.SkillId}; player resources untouched");
|
||
await Task.CompletedTask;
|
||
return true;
|
||
}
|
||
|
||
/// <summary>技能收尾:把冷冻再延长一点点,避免刚传送走的羊被别处逻辑打断。</summary>
|
||
internal static void RecordMechanikaSkillEnd4229938(TcpSession session, SceneMethods.ReportSkillEnd report)
|
||
{
|
||
if (!Mechanika4229938.TryGetValue(session, out var runtime)) return;
|
||
if (!runtime.States.TryGetValue(report.unitId, out var sheep) || report.skillId != sheep.SkillInstance) return;
|
||
sheep.ActionUntil = Environment.TickCount64 + 500;
|
||
}
|
||
|
||
private static async Task ClearMechanikaSheepUnderGate4229938(TcpSession session, MechanikaRuntime4229938 runtime)
|
||
{
|
||
var ids = runtime.States.Keys.ToArray();
|
||
runtime.States.Clear();
|
||
runtime.Owner = 0;
|
||
// 世界已经换代 ⇒ 旧 id 不要再往新世界送 Leave。
|
||
if (GetStateIfExists(session)?.WorldEntryControlGeneration != runtime.Generation) return;
|
||
foreach (var id in ids)
|
||
await session.NotifyAsync(MethodId.SyncLogicAgentLeave,
|
||
UxSerializer.Serialize(WorldCodec.LogicAgentLeave(id)), CancellationToken.None);
|
||
session.Log.Info($"[MECHANIKA] recalled count={ids.Length}");
|
||
}
|
||
|
||
/// <summary>对外统一召回入口(切角色 / 切场景)。</summary>
|
||
internal static async Task StopMechanikaSheep4229938(TcpSession session)
|
||
{
|
||
if (!Mechanika4229938.TryGetValue(session, out var runtime)) return;
|
||
await runtime.Gate.WaitAsync(CancellationToken.None);
|
||
try { await ClearMechanikaSheepUnderGate4229938(session, runtime); }
|
||
finally { runtime.Gate.Release(); }
|
||
}
|
||
}
|