薛之猫大王
2026-02-23 af74b152737e345cc59908bbceb87ff82a2f79a5
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
local CONST     = require 'config.const'
local Balance   = require 'config.balance'
 
local M = {}
 
-- ---------------------------------------------------------
-- 刺杀入口
-- ---------------------------------------------------------
function M.attempt(attacker, target)
    local cd = attacker:storage_get('kill_cd') or 0
    if cd > 0 then return false end
 
    if not attacker:is_in_radius(target, Balance.KILL_RANGE) then return false end
 
    if attacker:storage_get('is_disguising') then return false end
    if attacker:storage_get('is_dragging') then return false end
    if attacker:storage_get('is_attacking') then return false end
 
    local GameMgr = require 'core.game_manager'
    if GameMgr.get_state() == 'prepare' then return false end
 
    local backstab = M.is_backstab(attacker, target)
    local windup   = backstab and Balance.BACKSTAB_WINDUP or Balance.KILL_WINDUP
    local damage   = backstab and Balance.BACKSTAB_DAMAGE or Balance.KILL_DAMAGE
 
    attacker:play_animation('attack', 1.0, 0, -1, false, true)
    attacker:storage_set('is_attacking', true)
 
    clicli.timer.wait(windup, function()
        attacker:storage_set('is_attacking', false)
        attacker:stop_cur_animation()
 
        if not attacker:is_in_radius(target, Balance.KILL_RANGE + 0.5) then
            M.on_miss(attacker)
            return
        end
 
        if not target:is_alive() then return end
 
        M.on_hit(attacker, target, damage)
    end)
 
    return true
end
 
-- ---------------------------------------------------------
-- 命中处理
-- ---------------------------------------------------------
function M.on_hit(attacker, target, damage)
    if target:has_buff_by_key(CONST.BUFF_SHIELD) then
        target:remove_buffs_by_key(CONST.BUFF_SHIELD)
        M.set_cooldown(attacker, Balance.KILL_CD_HIT)
        return
    end
 
    attacker:damage({
        target  = target,
        damage  = damage,
        type    = CONST.DAMAGE_TYPE_ASSASSINATE,
        no_miss = true,
    })
 
    local Suspicion = require 'systems.suspicion'
    local witness_count = M.count_witnesses(attacker, 10)
    if witness_count > 0 then
        Suspicion.add(attacker, Balance.SUS_KILL_WITNESSED)
    else
        Suspicion.add(attacker, Balance.SUS_KILL_UNWITNESSED)
    end
 
    if not target:is_alive() then
        M.set_cooldown(attacker, Balance.KILL_CD_KILL)
        M.on_kill(attacker, target)
    else
        M.set_cooldown(attacker, Balance.KILL_CD_HIT)
    end
end
 
-- ---------------------------------------------------------
-- 未命中
-- ---------------------------------------------------------
function M.on_miss(attacker)
    local Suspicion = require 'systems.suspicion'
    Suspicion.add(attacker, Balance.SUS_KILL_MISS)
    M.set_cooldown(attacker, Balance.KILL_CD_MISS)
end
 
-- ---------------------------------------------------------
-- 击杀后处理
-- ---------------------------------------------------------
function M.on_kill(killer, victim)
    local is_silent = killer:has_buff_by_key(CONST.BUFF_SILENCER)
 
    if victim:has_tag('npc') then
        victim:add_tag('corpse')
        victim:set_recycle_on_remove(false)
 
        if not is_silent then
            local NpcMgr = require 'core.npc_manager'
            NpcMgr.trigger_panic(victim:get_point(), Balance.PANIC_RADIUS)
        end
 
        if is_silent then
            killer:remove_buffs_by_key(CONST.BUFF_SILENCER)
        end
 
        clicli.timer.wait(Balance.CORPSE_DECAY_TIME, function()
            if victim:has_tag('corpse') and not victim:has_tag('hidden') then
                victim:remove()
            end
        end)
    end
 
    if victim:has_tag('player_controlled') then
        local PlayerMgr = require 'core.player_manager'
        PlayerMgr.on_player_killed(killer, victim)
    end
end
 
-- ---------------------------------------------------------
-- 背刺判定
-- ---------------------------------------------------------
function M.is_backstab(attacker, target)
    local atk_point = attacker:get_point()
    local tgt_point = target:get_point()
    local tgt_face  = target:get_facing()
 
    local dx = atk_point:get_x() - tgt_point:get_x()
    local dy = atk_point:get_y() - tgt_point:get_y()
    local angle_to_attacker = math.deg(math.atan(dy, dx))
 
    local diff = math.abs(angle_to_attacker - tgt_face)
    if diff > 180 then diff = 360 - diff end
 
    return diff > 120
end
 
-- ---------------------------------------------------------
-- 目击者计数
-- ---------------------------------------------------------
function M.count_witnesses(attacker, radius)
    local area  = clicli.area.create_circle_area(attacker:get_point(), radius)
    local units = area:get_all_unit_in_area()
    area:remove()
 
    local count = 0
    for _, u in ipairs(units) do
        if u ~= attacker and u:is_alive() then
            if u:has_tag('player_controlled') or u:has_tag('npc') then
                count = count + 1
            end
        end
    end
    return count
end
 
-- ---------------------------------------------------------
-- 冷却
-- ---------------------------------------------------------
function M.set_cooldown(unit, duration)
    unit:storage_set('kill_cd', duration)
    clicli.timer.wait(duration, function()
        unit:storage_set('kill_cd', 0)
    end)
end
 
return M