薛之猫大王
2026-02-22 35593fff68c76413ce82a918e56f127355bf6c55
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 M = {}
 
local npc_list = {}
 
-- ---------------------------------------------------------
-- 初始化所有NPC
-- ---------------------------------------------------------
function M.init()
    local model_pool  = CONST.NPC_MODEL_POOL
    local spawn_areas = clicli.area.get_circle_areas_by_tag('npc_spawn')
    local roads       = M.collect_roads()
 
    for i = 1, CONST.NPC_COUNT do
        local npc_type = M.pick_npc_type()
        local spawn_area = spawn_areas[math.random(math.max(1, #spawn_areas))]
        local spawn = spawn_area and spawn_area:random_point() or clicli.point.create(0, 0)
        local model = #model_pool > 0 and model_pool[math.random(#model_pool)] or nil
 
        local unit_key = CONST.NPC_UNIT_KEYS[npc_type] or CONST.NPC_UNIT_KEYS.civilian
        local npc = clicli.unit.create_unit(CONST.NPC_PLAYER, unit_key, spawn, math.random(0, 360))
 
        if model then
            npc:replace_model(model)
        end
        npc:set_attr('最大生命', 1, '基础')
        npc:set_hp(1)
 
        npc:add_tag('npc')
        npc:add_tag('npc_' .. npc_type)
        npc:storage_set('npc_type',    npc_type)
        npc:storage_set('model_key',   model)
        npc:storage_set('is_panicked', false)
        npc:storage_set('ai_state',    'idle')
 
        if (npc_type == 'patrol' or npc_type == 'civilian') and #roads > 0 then
            local road = roads[math.random(#roads)]
            npc:move_along_road(road, 1, false, true, true)
            npc:storage_set('assigned_road', road)
        end
 
        if npc_type == 'vendor' then
            npc:stop()
        end
 
        if npc_type == 'wanderer' then
            M.start_wander(npc, spawn)
        end
 
        table.insert(npc_list, npc)
    end
end
 
-- ---------------------------------------------------------
-- NPC类型权重
-- ---------------------------------------------------------
function M.pick_npc_type()
    local r = math.random(100)
    if r <= 50 then return 'civilian'
    elseif r <= 70 then return 'patrol'
    elseif r <= 85 then return 'vendor'
    else return 'wanderer'
    end
end
 
-- ---------------------------------------------------------
-- 收集地图中的Road
-- ---------------------------------------------------------
function M.collect_roads()
    local roads = {}
    local tags = { 'road_main_loop', 'road_patrol', 'road_alley', 'road_square' }
    for _, tag in ipairs(tags) do
        local found = clicli.road.get_path_areas_by_tag(tag)
        for _, r in ipairs(found) do
            table.insert(roads, r)
        end
    end
    if #roads == 0 then
        roads = CONST.ROAD_LIST
    end
    return roads
end
 
-- ---------------------------------------------------------
-- 闲逛AI:在区域内随机移动
-- ---------------------------------------------------------
function M.start_wander(npc, center_point)
    local function wander_step()
        if not npc:is_alive() then return end
        if npc:storage_get('is_panicked') then return end
 
        local target = center_point:get_random_point(15)
        npc:move_to_pos(target)
 
        clicli.timer.wait(math.random(5, 12), function()
            wander_step()
        end)
    end
    wander_step()
end
 
-- ---------------------------------------------------------
-- 恐慌触发
-- ---------------------------------------------------------
function M.trigger_panic(source_point, radius)
    local panic_area = clicli.area.create_circle_area(source_point, radius)
    local units_in   = panic_area:get_all_unit_in_area()
    panic_area:remove()
 
    for _, u in ipairs(units_in) do
        if u:has_tag('npc') and u:is_alive() then
            u:storage_set('is_panicked', true)
 
            local flee_point = M.calc_flee_point(u:get_point(), source_point, 20)
            u:move_to_pos(flee_point)
 
            local recover_time = math.random(8, 12)
            clicli.timer.wait(recover_time, function()
                if not u:is_alive() then return end
                u:storage_set('is_panicked', false)
 
                local road = u:storage_get('assigned_road')
                if road then
                    u:move_along_road(road, 1, false, true, true)
                else
                    local npc_type = u:storage_get('npc_type')
                    if npc_type == 'vendor' then
                        u:stop()
                    end
                end
            end)
        end
    end
end
 
function M.calc_flee_point(npc_pos, danger_pos, distance)
    local dx = npc_pos:get_x() - danger_pos:get_x()
    local dy = npc_pos:get_y() - danger_pos:get_y()
    local len = math.sqrt(dx * dx + dy * dy)
    if len < 0.01 then dx, dy = 1, 0; len = 1 end
    local nx = dx / len * distance
    local ny = dy / len * distance
    return clicli.point.create(npc_pos:get_x() + nx, npc_pos:get_y() + ny)
end
 
-- ---------------------------------------------------------
-- 缩减NPC数量(紧迫期)
-- ---------------------------------------------------------
function M.reduce_npc_count(ratio)
    local remove_count = math.floor(#npc_list * ratio)
    for _ = 1, remove_count do
        if #npc_list == 0 then break end
        local idx = math.random(#npc_list)
        local npc = npc_list[idx]
        if npc:is_alive() then
            npc:remove()
        end
        table.remove(npc_list, idx)
    end
end
 
function M.get_npc_list()
    return npc_list
end
 
return M