Files
Ananta-ps/Ananta.Server/Ananta.Gameplay/CharacterCapabilities.cs

241 lines
13 KiB
C#
Raw Permalink 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 Ananta.Server.ClientData.Client4229938;
using Ananta.Server.Configuration;
namespace Ananta.Server.Gameplay;
/// <summary>
/// Character-owned capability buffs that unlock stock client modules.
///
/// Ownership source: <c>UrbanBadgeConfig.json</c> (ConfigDump_v3) — rows that set both
/// <c>Buff[]</c> and a non-zero <c>FightspiritId</c>. Of the 109 controllable spirits only 9
/// own anything; see <c>_role_capability_scan.txt</c> for the full list.
///
/// Everything else in that table is either a numeric passive (damage/HP/driving modifiers),
/// a skill variant, or an <c>OnlyServer</c> progress marker — none of them add exploration
/// verbs, so they are deliberately not published here.
///
/// Keep this list small: BuffConfig (4123 rows) also contains executable NPC, boss and
/// timeline actions that must never be published as player unlocks.
/// </summary>
internal static class CharacterCapabilities
{
/// <summary>
/// <c>UrbanJobConfig</c> 401 Professional Hacker. Authored per character in
/// <c>FightSpiritConfig.DefaultUrbanJob</c> — Seymour(15021023) and 赛默迪斯科球人(15022089)
/// carry it. Data-driven on purpose: the donor build hard-codes three spirit ids and
/// gets one of them wrong (15022061 is jobless).
/// </summary>
internal const uint HackerUrbanJob = 401;
/// <summary>Jobless marker from <c>UrbanJobConfig</c>; 0 is never a valid job id.</summary>
internal const uint JoblessUrbanJob = 100;
internal const uint DilaSpiritId = 15_020_997;
/// <summary>
/// 白靖 Bai Jing(spirit 15021039)。原生职业 = 赛车手(class 11300002 / job 4 Racer)。
/// 见 <c>GameRouter.BaiJingTime.cs</c> 文件头的完整取证。
/// </summary>
internal const uint BaiJingSpiritId = 15_021_039;
/// <summary>
/// BuffConfig 52959495 「可以激活斯安威斯坦」— Duration -1、IsHidden、Description「职业标记buff」。
/// 斯安威斯坦的<b>能力门闸</b>:客户端职业技能按钮(BattleMgr 一侧)查的就是这个 buff,
/// 不下发它,玩家就算挂着赛车手职业也看不到 / 按不动那个按钮。
/// </summary>
internal const uint BaiJingTimeScaleGateBuff = 52_959_495;
/// <summary>
/// BuffConfig 52959496 「斯安威斯坦」— Duration 5s、Description「子弹时间」、EffectIds [53500525]。
/// 真正生效那一份(5 秒子弹时间)。⛑ 它<b>不进</b>常驻清单:
/// 常驻清单的含义是「永久能力标记」,而这个是有时长的技能状态,
/// 由 <c>GameRouter.BaiJingTime.cs</c> 在玩家按下按钮时现发(Permanent=false)。
/// 塞进常驻会让玩家一登录就永久挂着子弹时间。
/// </summary>
internal const uint BaiJingTimeScaleActiveBuff = 52_959_496;
/// <summary>
/// BuffConfig 52606175 — 名字写的是「移动射击时造成伤害增加15%」,但它是
/// <b>徽章 19002851「You Talkin' to Me?」</b>(Cab Driver 出租车司机系列 19002850/51/52)
/// 唯一挂在身上的 buff,而捐赠端 7k-v0.5 把白靖的 <c>ResidentBuffIds</c> 就写成
/// <c>[52606175]</c> + <c>BadgeIds = [19002850, 19002851, 19002852]</c>。
///
/// ⛑ 为什么仍然下发它:客户端 <c>DriveControlDriverStore.ShowProfessionSkillButton</c> 查的
/// <c>BuffConfig.TimeScaleBadgeBuff</c> 我们读不到字面量(LuaGen/ClientConfig.lua 的键名是
/// 混淆过的哈希),52959495 与 52606175 都是候选:
/// · 52959495「可以激活斯安威斯坦」Duration -1,且出现在 <c>UberSimConfig.TimeStopPara</c>
/// (TimeStopPara 的另一条 52980302 名字直接写着「货车职业徽章…(子弹时间持续时间提高)」)
/// ⇒ <b>TimeStopPara 里的 BuffId 都是徽章 buff</b>,这一点偏向 52959495 = TimeScaleBadgeBuff;
/// · 但 52606175 是唯一字面意义上的「badge buff」。
/// 两个都下发即可覆盖两种可能;52606175 本身是一个隐藏的 +15% 移动射击伤害,无副作用。
/// </summary>
internal const uint BaiJingCabDriverBadgeBuff = 52_606_175;
/// <summary>
/// BuffConfig 52606125 「可以念力飞行」— permanent, hidden client-side capability flag.
/// Badge 19002141 "Zero-Gravity Allure", quality 4.
/// </summary>
internal const uint DilaFloatFlightBuff = 52_606_125;
/// <summary>
/// Hacker capability set, "safe" shape — the proven round-1 baseline (2026-09-20,
/// "middle-mouse entry visible but grey"). Kept selectable because the round-1 experiments
/// showed the middle-mouse GadgetManager entry can disappear on wider sets; switching
/// <c>gameplay.hackerAbilitySet</c> back to "safe" restores this exact shape without a rebuild.
///
/// Observed behaviour per set (all measured on the real client, Seymour 15021023):
/// | set | middle-mouse hack entry |
/// |---------------------------------------|-------------------------|
/// | 33/167/168 (this one, no battery) | visible but grey |
/// | 33/167/168 + 49/50 | gone |
/// | donor's 9 (33/134/135/137/161/167/168/178/179) | gone |
/// | donor's 9 + 52899024 | gone |
/// </summary>
internal static readonly uint[] HackerBuffsSafe =
[
52_606_133, // 黑客骇入能力总开关 — without it every other one stays inert.
52_606_167, // 黑客-骇入车流载具能力
52_606_168, // 骇入车流载具-加速技能
];
/// <summary>
/// Hacker capability set, "full" shape — everything job 401 can own (2026-09-20 explicit
/// request: unlock ALL of Seymour's hacker abilities, drone and spider included).
///
/// Layout (BuffConfig names, cross-checked against donor_7k + ConfigDump_v3):
/// · 52606133 黑客骇入能力总开关 — global gate, same as safe.
/// · 52606134/135/137/161 — atmosphere-NPC hack actions (distract / rebel / press).
/// · 52606167/168 — traffic-vehicle hacking + its rush skill.
/// · 52606178/179 — HackerConfig.HackNPCAbilityBuff[6..7] (EnemyDistract / EnemyRebel);
/// the ordinary-NPC flags alone yield an empty enemy menu, so these ship together.
/// · 52606149 蜘蛛机器人 / 52606150 黑客无人机 — the two EonBug-app skill entries.
/// ⛑️ NOT in donor_7k. They are what makes the spider/drone menu items cast
/// (HackScriptFunc.SpiderSkill/DroneSkill → UseSkillByPid 51938181/51938183); the same
/// two skills are authored on Seymour's own FightSpiritConfig row as TempSkill, so the
/// battle pipeline accepts them (IsKnownSkill covers SkillConfig).
/// Known risk from round 1: publishing 49/50 coincided with the middle-mouse entry
/// disappearing. The EonBug APP path (HackerAppPanelStore) does not consult these buffs
/// at all, so if that regression shows up again the app still works — flip
/// hackerAbilitySet to "safe" and the trio comes back.
/// </summary>
internal static readonly uint[] HackerBuffsFull =
[
52_606_133,
52_606_134,
52_606_135,
52_606_137,
52_606_161,
52_606_167,
52_606_168,
52_606_178,
52_606_179,
52_606_149, // 蜘蛛机器人 (UrbanBadgeConfig 19001108, job-401 award)
52_606_150, // 黑客无人机 (UrbanBadgeConfig 19001109, job-401 award)
52_606_170, // 黑客-俄罗斯方块大停电能力 (2026-09-23 补;Donor Blackout §6.1 要的就是这条,
// 名字在 BuffConfig 里逐字对得上)。注意别和 T0-12 的
// 52606118「可以横梁刺杀」混 —— 那是完全不同的能力。
];
/// <summary>
/// Talent-tree ability buffs (see <c>gameplay.hackerTalentBuffIds</c>). Configurable rather than
/// authored here because they are not yet a proven shape — T1-6 only lands the C2S round-trip,
/// so publishing them here is the flat replacement, not an actual talent grant.
/// </summary>
private static IReadOnlyList<uint> TalentBuffIds
=> PrivateServerConfigStore.Current.Gameplay.HackerTalentBuffIds;
/// <summary>How many talent buffs actually landed, for the publish log.</summary>
internal static int TalentBuffCount => TalentBuffIds.Count;
/// <summary><c>HackerHackTypeConfig</c> prices hacks in battery points; the donor build seeds 6/6.</summary>
internal const uint HackerBatteryCapacity = 6;
/// <summary>
/// BuffConfig 52606233 「潜水服buff」— 不会掉氧气,且会更快的恢复氧气。
/// Published to <b>every</b> controllable spirit (<c>gameplay.divingMode == "on"</c>) because
/// diving is a client-global capability, not a character one: the gate lives in
/// <c>DiveManager.CheckIsEnableDiving()</c> and oxygen is a player attribute, so every role
/// needs the same suit. Id is configurable so an id that turns out to be wrong on a future
/// build can be swapped without a rebuild.
/// </summary>
internal const uint DivingSuitBuff = 52_606_233;
/// <summary>
/// Character-independent capability buffs: the diving suit when <c>gameplay.divingMode</c> is
/// "on", plus the Cat Express support buffs (<c>gameplay.deliverySupportBuffIds</c>).
///
/// The courier half is here for one reason: <c>RefreshCallFunctionListView</c> filters a support
/// button out unless the player carries its <c>UberSimOrderSupportConfig.UnlockBuff</c>
/// (52800660 叫车 / 52800661 无人机). Publishing them with the regular spirit buff snapshot means
/// they are present in the app and re-published on every character switch — no dedicated S2C.
/// Set the list to <c>[]</c> to restore the pre-2026-09-24 buff snapshot byte-for-byte.
/// </summary>
internal static IReadOnlyList<uint> GlobalCapabilityBuffIds
{
get
{
var gameplay = PrivateServerConfigStore.Current.Gameplay;
List<uint> ids = [];
if (string.Equals(gameplay.DivingMode, "on", StringComparison.OrdinalIgnoreCase))
{
var suit = gameplay.DivingSuitBuffId;
if (suit != 0)
ids.Add(suit);
}
foreach (var id in gameplay.DeliverySupportBuffIds ?? [])
{
if (id != 0 && !ids.Contains(id))
ids.Add(id);
}
return ids;
}
}
/// <summary>Authored urban job of a controllable spirit; 0 normalizes to Jobless(100).</summary>
internal static uint UrbanJobOf(uint templateId)
{
var job = ClientConfigRepository.Characters()
.FirstOrDefault(x => x.TemplateId == templateId)?.DefaultUrbanJob ?? 0;
return job == 0 ? JoblessUrbanJob : job;
}
/// <summary>True when the character's authored job grants the hacking kit.</summary>
internal static bool CanHack(uint templateId) => UrbanJobOf(templateId) == HackerUrbanJob;
/// <summary>
/// Capability buffs for a character. <paramref name="fullHackerKit"/> selects between the
/// safe trio and the full 11-buff hacker set (see <see cref="HackerBuffsSafe"/> /
/// <see cref="HackerBuffsFull"/>); the caller reads it from
/// <c>gameplay.hackerAbilitySet</c> so the shape stays runtime-switchable.
/// </summary>
internal static IReadOnlyList<uint> BuffIds(uint templateId, bool fullHackerKit = false)
{
var traversal = WebTraversal.CapabilityBuffIds(templateId);
IEnumerable<uint> characterCapabilities = templateId switch
{
DilaSpiritId => new uint[] { DilaFloatFlightBuff },
// 斯安威斯坦的能力门闸(永久标记,不是技能本体 —— 见常量注释)
// + 出租车司机徽章 19002851 的 buff(对齐捐赠端 ResidentBuffIds,见常量注释)
// + 排查旋钮 <c>gameplay.baiJingTimeScaleProbeBuffs</c>(默认空,见配置注释)。
BaiJingSpiritId => new uint[] { BaiJingTimeScaleGateBuff, BaiJingCabDriverBadgeBuff }
.Concat(PrivateServerConfigStore.Current.Gameplay.BaiJingTimeScaleProbeBuffs ?? Array.Empty<uint>()),
_ => Array.Empty<uint>()
};
// Diving suit is role-independent: it rides the same snapshot as the traversal kit so it
// needs no extra packet and survives a role switch (the snapshot is republished per switch).
characterCapabilities = characterCapabilities.Concat(GlobalCapabilityBuffIds);
if (CanHack(templateId))
{
characterCapabilities = characterCapabilities.Concat(fullHackerKit ? HackerBuffsFull : HackerBuffsSafe);
// Talent layer rides the same switch as the rest of the widened set, so flipping
// hackerAbilitySet to "safe" rolls back to the proven round-1 shape in one edit.
if (fullHackerKit)
characterCapabilities = characterCapabilities.Concat(TalentBuffIds);
}
return traversal.Concat(characterCapabilities).Distinct().ToArray();
}
}