using Ananta.Server.ClientData.Client4229938;
using Ananta.Server.Configuration;
namespace Ananta.Server.Gameplay;
///
/// Character-owned capability buffs that unlock stock client modules.
///
/// Ownership source: UrbanBadgeConfig.json (ConfigDump_v3) — rows that set both
/// Buff[] and a non-zero FightspiritId. Of the 109 controllable spirits only 9
/// own anything; see _role_capability_scan.txt for the full list.
///
/// Everything else in that table is either a numeric passive (damage/HP/driving modifiers),
/// a skill variant, or an OnlyServer 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.
///
internal static class CharacterCapabilities
{
///
/// UrbanJobConfig 401 Professional Hacker. Authored per character in
/// FightSpiritConfig.DefaultUrbanJob — 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).
///
internal const uint HackerUrbanJob = 401;
/// Jobless marker from UrbanJobConfig; 0 is never a valid job id.
internal const uint JoblessUrbanJob = 100;
internal const uint DilaSpiritId = 15_020_997;
///
/// 白靖 Bai Jing(spirit 15021039)。原生职业 = 赛车手(class 11300002 / job 4 Racer)。
/// 见 GameRouter.BaiJingTime.cs 文件头的完整取证。
///
internal const uint BaiJingSpiritId = 15_021_039;
///
/// BuffConfig 52959495 「可以激活斯安威斯坦」— Duration -1、IsHidden、Description「职业标记buff」。
/// 斯安威斯坦的能力门闸:客户端职业技能按钮(BattleMgr 一侧)查的就是这个 buff,
/// 不下发它,玩家就算挂着赛车手职业也看不到 / 按不动那个按钮。
///
internal const uint BaiJingTimeScaleGateBuff = 52_959_495;
///
/// BuffConfig 52959496 「斯安威斯坦」— Duration 5s、Description「子弹时间」、EffectIds [53500525]。
/// 真正生效那一份(5 秒子弹时间)。⛑ 它不进常驻清单:
/// 常驻清单的含义是「永久能力标记」,而这个是有时长的技能状态,
/// 由 GameRouter.BaiJingTime.cs 在玩家按下按钮时现发(Permanent=false)。
/// 塞进常驻会让玩家一登录就永久挂着子弹时间。
///
internal const uint BaiJingTimeScaleActiveBuff = 52_959_496;
///
/// BuffConfig 52606175 — 名字写的是「移动射击时造成伤害增加15%」,但它是
/// 徽章 19002851「You Talkin' to Me?」(Cab Driver 出租车司机系列 19002850/51/52)
/// 唯一挂在身上的 buff,而捐赠端 7k-v0.5 把白靖的 ResidentBuffIds 就写成
/// [52606175] + BadgeIds = [19002850, 19002851, 19002852]。
///
/// ⛑ 为什么仍然下发它:客户端 DriveControlDriverStore.ShowProfessionSkillButton 查的
/// BuffConfig.TimeScaleBadgeBuff 我们读不到字面量(LuaGen/ClientConfig.lua 的键名是
/// 混淆过的哈希),52959495 与 52606175 都是候选:
/// · 52959495「可以激活斯安威斯坦」Duration -1,且出现在 UberSimConfig.TimeStopPara
/// (TimeStopPara 的另一条 52980302 名字直接写着「货车职业徽章…(子弹时间持续时间提高)」)
/// ⇒ TimeStopPara 里的 BuffId 都是徽章 buff,这一点偏向 52959495 = TimeScaleBadgeBuff;
/// · 但 52606175 是唯一字面意义上的「badge buff」。
/// 两个都下发即可覆盖两种可能;52606175 本身是一个隐藏的 +15% 移动射击伤害,无副作用。
///
internal const uint BaiJingCabDriverBadgeBuff = 52_606_175;
///
/// BuffConfig 52606125 「可以念力飞行」— permanent, hidden client-side capability flag.
/// Badge 19002141 "Zero-Gravity Allure", quality 4.
///
internal const uint DilaFloatFlightBuff = 52_606_125;
///
/// 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
/// gameplay.hackerAbilitySet 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 |
///
internal static readonly uint[] HackerBuffsSafe =
[
52_606_133, // 黑客骇入能力总开关 — without it every other one stays inert.
52_606_167, // 黑客-骇入车流载具能力
52_606_168, // 骇入车流载具-加速技能
];
///
/// 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.
///
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「可以横梁刺杀」混 —— 那是完全不同的能力。
];
///
/// Talent-tree ability buffs (see gameplay.hackerTalentBuffIds). 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.
///
private static IReadOnlyList TalentBuffIds
=> PrivateServerConfigStore.Current.Gameplay.HackerTalentBuffIds;
/// How many talent buffs actually landed, for the publish log.
internal static int TalentBuffCount => TalentBuffIds.Count;
/// HackerHackTypeConfig prices hacks in battery points; the donor build seeds 6/6.
internal const uint HackerBatteryCapacity = 6;
///
/// BuffConfig 52606233 「潜水服buff」— 不会掉氧气,且会更快的恢复氧气。
/// Published to every controllable spirit (gameplay.divingMode == "on") because
/// diving is a client-global capability, not a character one: the gate lives in
/// DiveManager.CheckIsEnableDiving() 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.
///
internal const uint DivingSuitBuff = 52_606_233;
///
/// Character-independent capability buffs: the diving suit when gameplay.divingMode is
/// "on", plus the Cat Express support buffs (gameplay.deliverySupportBuffIds).
///
/// The courier half is here for one reason: RefreshCallFunctionListView filters a support
/// button out unless the player carries its UberSimOrderSupportConfig.UnlockBuff
/// (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 [] to restore the pre-2026-09-24 buff snapshot byte-for-byte.
///
internal static IReadOnlyList GlobalCapabilityBuffIds
{
get
{
var gameplay = PrivateServerConfigStore.Current.Gameplay;
List 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;
}
}
/// Authored urban job of a controllable spirit; 0 normalizes to Jobless(100).
internal static uint UrbanJobOf(uint templateId)
{
var job = ClientConfigRepository.Characters()
.FirstOrDefault(x => x.TemplateId == templateId)?.DefaultUrbanJob ?? 0;
return job == 0 ? JoblessUrbanJob : job;
}
/// True when the character's authored job grants the hacking kit.
internal static bool CanHack(uint templateId) => UrbanJobOf(templateId) == HackerUrbanJob;
///
/// Capability buffs for a character. selects between the
/// safe trio and the full 11-buff hacker set (see /
/// ); the caller reads it from
/// gameplay.hackerAbilitySet so the shape stays runtime-switchable.
///
internal static IReadOnlyList BuffIds(uint templateId, bool fullHackerKit = false)
{
var traversal = WebTraversal.CapabilityBuffIds(templateId);
IEnumerable characterCapabilities = templateId switch
{
DilaSpiritId => new uint[] { DilaFloatFlightBuff },
// 斯安威斯坦的能力门闸(永久标记,不是技能本体 —— 见常量注释)
// + 出租车司机徽章 19002851 的 buff(对齐捐赠端 ResidentBuffIds,见常量注释)
// + 排查旋钮 gameplay.baiJingTimeScaleProbeBuffs(默认空,见配置注释)。
BaiJingSpiritId => new uint[] { BaiJingTimeScaleGateBuff, BaiJingCabDriverBadgeBuff }
.Concat(PrivateServerConfigStore.Current.Gameplay.BaiJingTimeScaleProbeBuffs ?? Array.Empty()),
_ => Array.Empty()
};
// 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();
}
}