241 lines
13 KiB
C#
241 lines
13 KiB
C#
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();
|
||
}
|
||
}
|