diff --git a/Assets/Scripts/CECHostPlayer.Skill.cs b/Assets/Scripts/CECHostPlayer.Skill.cs
index 0f87543568..72d1d6458e 100644
--- a/Assets/Scripts/CECHostPlayer.Skill.cs
+++ b/Assets/Scripts/CECHostPlayer.Skill.cs
@@ -17,6 +17,70 @@ namespace BrewMonster
{
public partial class CECHostPlayer
{
+ ///
+ /// Anti-spam gate for (client-side input flood control).
+ ///
+ /// Purpose:
+ /// - Prevents rapid repeated shortcut-trigger calls (mouse/touch spam, key repeat, UI double-fire)
+ /// from spamming trace/cast requests and causing unstable client behavior.
+ ///
+ ///
+ /// Design:
+ /// - A small global minimum interval (ANY) blocks ultra-high-frequency bursts across all skills.
+ /// - A per-skill minimum interval (PER_SKILL) blocks repeatedly pressing the same skill rapidly.
+ /// - Exception: "press again to release a charging skill immediately" is allowed (not blocked),
+ /// so charge mechanics remain responsive.
+ ///
+ ///
+ /// Notes:
+ /// - Uses so throttling remains consistent under timeScale changes.
+ /// - This is client-side hygiene only; server cooldown/validation is still authoritative.
+ ///
+ ///
+ ///
+ /// ApplySkillShortcut 的反刷 (客户端输入洪泛控制)
+ ///
+ /// 目的:
+ /// - 防止鼠标/触摸狂点、按键连发、UI 重复触发导致的频繁施法请求,避免客户端不稳定。
+ ///
+ /// 设计:
+ /// - ANY:对所有技能共用的极短间隔,过滤超高频 burst。
+ /// - PER_SKILL:对同一技能的短间隔,过滤同技能连点。
+ /// - 例外:充能技能“再次按下立即释放”不会被拦截,保证充能手感。
+ ///
+ /// 备注:
+ /// - 使用 Time.unscaledTime,避免 timeScale 变化导致反刷失效。
+ /// - 仅客户端防抖/限流,服务器校验与冷却依然为准。
+ ///
+ private const float APPLY_SKILL_SHORTCUT_MIN_INTERVAL_ANY = 0.05f;
+ private const float APPLY_SKILL_SHORTCUT_MIN_INTERVAL_PER_SKILL = 0.12f;
+
+ private float _applySkillShortcut_lastAnyTime = -999f;
+ private readonly Dictionary _applySkillShortcut_lastSkillTime = new Dictionary(64);
+
+ private bool ShouldBlockApplySkillShortcutSpam(int idSkill, bool allowChargeRelease)
+ {
+ // Use unscaled time so spam protection still works during slow-mo/timeScale changes.
+ // 使用 unscaledTime,避免 timeScale 变化导致反刷失效
+ float now = Time.unscaledTime;
+
+ if (now - _applySkillShortcut_lastAnyTime < APPLY_SKILL_SHORTCUT_MIN_INTERVAL_ANY)
+ return true;
+
+ if (!allowChargeRelease)
+ {
+ if (_applySkillShortcut_lastSkillTime.TryGetValue(idSkill, out float lastSkillTime))
+ {
+ if (now - lastSkillTime < APPLY_SKILL_SHORTCUT_MIN_INTERVAL_PER_SKILL)
+ return true;
+ }
+ }
+
+ _applySkillShortcut_lastAnyTime = now;
+ _applySkillShortcut_lastSkillTime[idSkill] = now;
+ return false;
+ }
+
public struct SkillShortCutConfig
{
public int setNum;
@@ -506,8 +570,15 @@ namespace BrewMonster
//// If we press a chargeable skill again when it's being charged,
//// we cast it out at once
- if (IsSpellingMagic() && m_pCurSkill != null && m_pCurSkill.IsCharging() &&
- m_pCurSkill.GetSkillID() == pSkill.GetSkillID())
+ bool allowChargeRelease = IsSpellingMagic() && m_pCurSkill != null && m_pCurSkill.IsCharging() &&
+ m_pCurSkill.GetSkillID() == pSkill.GetSkillID();
+
+ // Anti-spam: block rapid-fire shortcut calls (except charge-release press)
+ // 反刷:阻止短时间内狂点快捷键(充能释放第二次按下除外)
+ if (ShouldBlockApplySkillShortcutSpam(idSkill, allowChargeRelease))
+ return false;
+
+ if (allowChargeRelease)
{
m_pCurSkill.EndCharging();
UnityGameSession.c2s_SendCmdContinueAction();
diff --git a/Docs/ApplySkillShortcut-AntiSpam.md b/Docs/ApplySkillShortcut-AntiSpam.md
new file mode 100644
index 0000000000..061b10e66b
--- /dev/null
+++ b/Docs/ApplySkillShortcut-AntiSpam.md
@@ -0,0 +1,30 @@
+# ApplySkillShortcut Anti-spam
+
+## What it is
+`CECHostPlayer.ApplySkillShortcut(...)` has a small client-side anti-spam gate to prevent input floods (touch/mouse spam, key repeat, UI double-fire) from generating excessive trace/cast work.
+
+This is **not** a replacement for server cooldowns or validation. It’s client-side hygiene to keep the client stable and reduce redundant requests.
+
+## How it works
+The gate is implemented inside `Assets/Scripts/CECHostPlayer.Skill.cs`:
+
+- **Global minimum interval (ANY)**: blocks ultra-high-frequency bursts across *all* skills.
+- **Per-skill minimum interval (PER_SKILL)**: blocks rapidly pressing the *same* skill repeatedly.
+- **Charge exception**: if the player is charging a skill and presses the **same** skill again to release immediately, that press is **allowed** (so charging remains responsive).
+
+The timing source is `Time.unscaledTime` so the throttle stays consistent even if `Time.timeScale` changes.
+
+## Tunables
+In `CECHostPlayer.Skill.cs`:
+
+- `APPLY_SKILL_SHORTCUT_MIN_INTERVAL_ANY` (default `0.05s`)
+- `APPLY_SKILL_SHORTCUT_MIN_INTERVAL_PER_SKILL` (default `0.12s`)
+
+If you need to tweak feel:
+
+- Lower **ANY** carefully; it protects against accidental double-fire from UI/input systems.
+- Lower **PER_SKILL** if skills feel “eaten” during fast tapping, but keep it high enough to block spam.
+
+## Why the charge exception exists
+Chargeable skills commonly use a second press to “cast now”. Blocking that second press makes charging feel broken, so the anti-spam explicitly does not block it.
+