返回博客
Guide4 分钟阅读

FiveM 监狱系统:脚本实现有意义的监禁时间

构建玩家真正参与的监狱系统。监狱工作、逃脱路线、守卫、假释以及深度角色扮演的顶级 FiveM 监狱脚本。

Agency Scripts

Agency Scripts 创始人兼首席开发者

监狱系统架构

监狱系统是您可以添加到 FiveM 角色扮演服务器中最具影响力的功能之一,因为它为犯罪行为创造了有意义的后果,同时为被监禁玩家提供了有趣的活动,避免他们无聊地盯着墙壁三十分钟。架构围绕实时倒计时的监狱计时器、一系列允许囚犯通过劳动减刑的监狱活动,以及限制玩家在服刑期间留在监狱范围内的边界系统。GTA V 中的 Bolingbroke Penitentiary 是标准选择,因为它拥有完整建模的内部,包括牢房区、院子和周边设施。您的系统需要三个核心组件:服务器端的判刑逻辑,分配监禁时间并剥夺武器和违禁品;客户端的边界执行,玩家离开监狱边界时传送回监狱;以及为囚犯提供有意义任务以打发时间的活动框架。

数据库架构与判决

数据库跟踪有效刑期、刑期历史(犯罪记录)和囚犯活动进展。将刑期结束时间存为绝对时间戳,而非剩余时长,以便即使玩家离线,时间仍持续倒计时,防止玩家通过下线暂停刑期的漏洞。包含逮捕警官、指控和原始刑期长度字段以备记录:

CREATE TABLE IF NOT EXISTS prison_sentences (
    id INT AUTO_INCREMENT PRIMARY KEY,
    citizenid VARCHAR(50) NOT NULL,
    sentence_minutes INT NOT NULL,
    time_served INT DEFAULT 0,
    reduction_earned INT DEFAULT 0,
    jailed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    release_at TIMESTAMP NOT NULL,
    charges TEXT DEFAULT NULL,
    arresting_officer VARCHAR(50) DEFAULT NULL,
    status ENUM('active', 'released', 'escaped', 'pardoned') DEFAULT 'active',
    parole_eligible BOOLEAN DEFAULT FALSE,
    INDEX idx_citizen (citizenid),
    INDEX idx_status (status)
);

CREATE TABLE IF NOT EXISTS prison_activity_log (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    citizenid VARCHAR(50) NOT NULL,
    sentence_id INT NOT NULL,
    activity_type ENUM('mining', 'gym', 'cleaning', 'library', 'yard') NOT NULL,
    reduction_minutes INT DEFAULT 0,
    completed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (sentence_id) REFERENCES prison_sentences(id)
);

判刑玩家

当警察使用 jail 命令时,服务器验证警员权限,根据指控计算刑期,剥夺犯人武器及非法物品,并传送至监狱内出生点。刑期必须立即持久化到数据库,以保证服务器重启后依然有效。将判刑实现为仅授权警员可执行的服务器端命令:

RegisterCommand('jail', function(source, args)
    local src = source
    local Officer = QBCore.Functions.GetPlayer(src)
    if not Officer then return end

    -- Check police job
    if Officer.PlayerData.job.name ~= 'police' then
        TriggerClientEvent('QBCore:Notify', src, 'Unauthorized', 'error')
        return
    end

    local targetId = tonumber(args[1])
    local minutes = tonumber(args[2])
    local charges = table.concat(args, ' ', 3)

    if not targetId or not minutes or minutes <= 0 then
        TriggerClientEvent('QBCore:Notify', src, 'Usage: /jail [id] [minutes] [charges]', 'error')
        return
    end

    local Target = QBCore.Functions.GetPlayer(targetId)
    if not Target then
        TriggerClientEvent('QBCore:Notify', src, 'Player not found', 'error')
        return
    end

    local citizenid = Target.PlayerData.citizenid
    local releaseAt = os.date('!%Y-%m-%d %H:%M:%S', os.time() + (minutes * 60))

    -- Insert sentence
    local sentenceId = MySQL.insert.await(
        'INSERT INTO prison_sentences (citizenid, sentence_minutes, release_at, charges, arresting_officer) VALUES (?, ?, ?, ?, ?)',
        { citizenid, minutes, releaseAt, charges, Officer.PlayerData.citizenid }
    )

    -- Strip weapons and contraband
    local contraband = {'weapon_pistol', 'weapon_smg', 'lockpick', 'thermite'}
    for _, item in ipairs(contraband) do
        local playerItem = Target.Functions.GetItemByName(item)
        if playerItem then
            Target.Functions.RemoveItem(item, playerItem.amount)
        end
    end

    -- Set jail metadata and teleport
    Target.Functions.SetMetaData('injail', sentenceId)
    TriggerClientEvent('prison:client:enter', targetId, sentenceId, minutes)
    TriggerClientEvent('QBCore:Notify', src, 'Jailed ' .. GetPlayerName(targetId) .. ' for ' .. minutes .. ' minutes', 'success')
end, false)

监狱活动

监狱活动是让被关押玩家保持参与的核心游戏循环。没有这些活动,服刑时间就是惩罚,会导致玩家下线,这对服务器人口不利。设计活动时应提供权衡:执行劳动可减少刑期,赋予囚犯对其体验的控制权。每项活动应有冷却时间以防止刷屏,并设有每个活动周期的最大减刑上限,确保60分钟刑期不会在5分钟内清除。最常见的活动包括采矿场采矿、健身房锻炼、清洁设施和在监狱图书馆度过时间。每项活动使用不同的小游戏或交互模式以提供多样性:

Config.PrisonActivities = {
    mining = {
        label = 'Quarry Mining',
        coords = vector3(1690.64, 2592.63, 45.56),
        radius = 15.0,
        reductionPerCycle = 2,   -- minutes reduced per completion
        cycleDuration = 45,       -- seconds per mining cycle
        cooldown = 30,            -- seconds between cycles
        maxReductionPerSession = 10,
        animation = { dict = 'amb@world_human_hammering@male@base', anim = 'base' },
        requiredProp = 'prop_tool_pickaxe',
    },
    gym = {
        label = 'Prison Gym',
        coords = vector3(1662.67, 2527.84, 45.56),
        radius = 10.0,
        reductionPerCycle = 1,
        cycleDuration = 30,
        cooldown = 60,
        maxReductionPerSession = 6,
        animation = { dict = 'amb@world_human_muscle_free_weights@male@barbell@base', anim = 'base' },
    },
    cleaning = {
        label = 'Facility Cleaning',
        coords = vector3(1653.18, 2499.26, 45.56),
        radius = 20.0,
        reductionPerCycle = 1,
        cycleDuration = 25,
        cooldown = 20,
        maxReductionPerSession = 8,
        animation = { dict = 'amb@world_human_janitor@male@base', anim = 'base' },
        requiredProp = 'prop_cs_broom',
    },
    library = {
        label = 'Prison Library',
        coords = vector3(1680.21, 2513.45, 45.56),
        radius = 8.0,
        reductionPerCycle = 1,
        cycleDuration = 60,
        cooldown = 45,
        maxReductionPerSession = 4,
        animation = { dict = 'amb@world_human_clipboard@male@base', anim = 'base' },
    },
}

-- Server-side activity completion handler
RegisterNetEvent('prison:server:completeActivity', function(activityType, sentenceId)
    local src = source
    local Player = QBCore.Functions.GetPlayer(src)
    if not Player then return end

    local citizenid = Player.PlayerData.citizenid
    local activity = Config.PrisonActivities[activityType]
    if not activity then return end

    -- Validate sentence is active
    local sentence = MySQL.single.await(
        'SELECT * FROM prison_sentences WHERE id = ? AND citizenid = ? AND status = "active"',
        { sentenceId, citizenid }
    )
    if not sentence then return end

    -- Check session reduction cap
    local sessionReduction = MySQL.scalar.await(
        'SELECT COALESCE(SUM(reduction_minutes), 0) FROM prison_activity_log WHERE sentence_id = ? AND activity_type = ? AND completed_at > DATE_SUB(NOW(), INTERVAL 1 HOUR)',
        { sentenceId, activityType }
    )
    if sessionReduction >= activity.maxReductionPerSession then
        TriggerClientEvent('QBCore:Notify', src, 'Maximum reduction reached for this activity', 'error')
        return
    end

    -- Apply reduction
    local reduction = activity.reductionPerCycle
    MySQL.update('UPDATE prison_sentences SET reduction_earned = reduction_earned + ?, release_at = DATE_SUB(release_at, INTERVAL ? MINUTE) WHERE id = ?',
        { reduction, reduction, sentenceId })

    MySQL.insert('INSERT INTO prison_activity_log (citizenid, sentence_id, activity_type, reduction_minutes) VALUES (?, ?, ?, ?)',
        { citizenid, sentenceId, activityType, reduction })

    TriggerClientEvent('QBCore:Notify', src, 'Sentence reduced by ' .. reduction .. ' minute(s)', 'success')
    TriggerClientEvent('prison:client:updateTimer', src, reduction)
end)

假释系统

假释系统在监狱和完全自由之间增加了一层角色扮演。一旦囚犯服刑达到可配置的百分比,通常为 60-75%,他们就有资格获得假释。假释可由法官或高级警官通过命令授予,提前释放玩家但施加限制。假释玩家可能有宵禁,要求在特定时间段内待在其财产内,需定期向假释官报到,并禁止持有武器或进入某些区域。如果假释玩家违反任何条件,将自动被送回监狱,服完剩余刑期并加罚违规时间。在玩家元数据中跟踪假释状态,并在服务器上定期检查以验证条件遵守情况。

越狱事件

越狱是全服事件,为罪犯和执法者创造高风险角色扮演。越狱应需要大量准备、多名参与者,并给予警方足够警告以做出响应。将其实现为多阶段事件:外部团队首先必须获取特定物品,如直升机、爆炸物和伪装。然后他们必须通过外部控制面板的困难小游戏破解监狱安全系统。安全系统瘫痪后,牢门在有限时间内打开,囚犯可通过预定路线尝试逃脱,而守卫NPC和响应警察试图阻止他们。系统仅在在线警察人数达到最低要求时允许越狱,以确保公平响应:

Config.PrisonBreak = {
    minPoliceOnline = 5,
    cooldown = 28800, -- 8 hours between break attempts
    requiredItems = {
        { name = 'electronickit', amount = 2, label = 'Electronic Kit' },
        { name = 'thermite', amount = 3, label = 'Thermite Charge' },
    },
    phases = {
        {
            name = 'hack_security',
            label = 'Hack Security Grid',
            coords = vector3(1746.23, 2488.90, 45.80),
            duration = 60,
            minigame = { type = 'hack', difficulty = 'hard', attempts = 3 },
        },
        {
            name = 'breach_wall',
            label = 'Breach Perimeter Wall',
            coords = vector3(1651.78, 2569.33, 45.56),
            duration = 30,
            minigame = { type = 'thermite', time = 10 },
        },
        {
            name = 'disable_lockdown',
            label = 'Disable Lockdown Protocol',
            coords = vector3(1691.45, 2565.12, 45.56),
            duration = 45,
            minigame = { type = 'hack', difficulty = 'expert', attempts = 2 },
        },
    },
    escapeWindow = 300, -- 5 minutes to escape after all phases complete
    escapeRoutes = {
        { label = 'Main Gate', coords = vector3(1845.12, 2585.87, 45.67) },
        { label = 'Drainage Tunnel', coords = vector3(1617.34, 2523.90, 44.12) },
        { label = 'Helipad', coords = vector3(1700.89, 2565.34, 52.34) },
    },
}

守卫NPC系统

守卫NPC为监狱周围提供环境安全,防止囚犯随意走出。将守卫放置在主要关卡如主门、牢房入口和围墙周围。每个守卫应有一个带有路径点的巡逻路线,循环巡逻,造成某些区域存在无人看守的时间窗口。守卫检测离开授权区域的囚犯,并通过口头警告回应,若囚犯不服从则追击并攻击。使用考虑视线的接近检测实现守卫侦测,使囚犯可通过掩护偷偷绕过守卫。在越狱事件中,在关键位置生成额外守卫并提高其攻击性。使用FiveM原生函数 TaskPatrolSetPedCombatAttributes 赋予守卫真实的巡逻行为。确保将他们的关系组设置为对囚犯敌对,同时对警察和访客保持中立,防止在监狱暴动响应期间发生误伤。

边界执行与释放

边界系统使用覆盖整个设施的多边形区域将囚犯限制在监狱范围内。当有刑期的玩家移出该区域时,会收到警告通知,并有10秒时间返回,否则将被传送回监狱出生点,且因逃跑尝试增加额外刑期。客户端每2秒运行一次检查,将玩家位置与监狱边界多边形比较。释放时,服务器每30秒运行一次计时循环检查所有有效刑期。刑期到期时,系统更新数据库状态为释放,清除玩家监狱元数据,若有预监狱备份则恢复库存,并传送至监狱出口,通知玩家获释。若玩家离线时刑期到期,释放将在其下次登录时通过玩家加载事件检查过期刑期处理。包含警察手动提前释放玩家的命令,适用于误判或通过角色扮演协商的认罪协议。

与警察及法律系统集成

您的监狱系统应与警方 MDT 及服务器上的任何法庭系统紧密集成。当玩家被监禁时,其 MDT 中的犯罪记录应自动更新指控、刑期和逮捕警官信息。如果您的服务器有法庭系统,法官判决的刑期应通过相同的监禁功能执行以确保一致性。实现刑期申诉系统,囚犯可从监狱图书馆提交申诉,生成供法官审查的工单,可能导致减刑或提前释放。通过统计每个公民被监禁次数跟踪累犯情况,并利用此数据通过乘数系统对重复犯罪者实施更严厉的判刑。导出您的监禁功能,使法庭系统、自动犯罪检测或管理员命令等其他资源均可通过相同验证路径将玩家送入监狱。

准备好开始了吗?

在我们的商店获取脚本,或加入 Discord 获取支持、更新以及新功能预告。