diff --git a/.gitignore b/.gitignore index 21dfb9bf..a2adfee9 100644 --- a/.gitignore +++ b/.gitignore @@ -165,3 +165,11 @@ cython_debug/ # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ +/ext/Client/nppBackup +/ext/Server/BotChatter/nppBackup +/ext/Server/BotChatter/Packs/nppBackup +/ext/Server/nppBackup +/ext/Shared/Constants/nppBackup +/ext/Shared/Names/nppBackup +/ext/Shared/Names/packs/nppBackup +/ext/Shared/nppBackup diff --git a/ext/Client/BotChatterClient.lua b/ext/Client/BotChatterClient.lua new file mode 100644 index 00000000..25260fb7 --- /dev/null +++ b/ext/Client/BotChatterClient.lua @@ -0,0 +1,193 @@ +-- ext/Client/BotChatterClient.lua +-- Code by: JMDigital (https://github.com/JenkinsTR) +-- Bot chatter overlay with DebugRenderer (no WebUI, no ChatManager) top-left, left-aligned, newest on top. +-- Bigger, team-tinted text, colored speaker names, and mention highlights. + +local VIS = { Global = 0, Team = 1, Squad = 2 } + +-- --------------- State & helpers --------------- +local messages = {} + +local function now() return math.floor(SharedUtils:GetTimeMS()) / 1000.0 end + +local function me() return PlayerManager:GetLocalPlayer() end + +local function visible_to_me(vis, teamId, squadId) + local p = me() + if not p then return false end + if vis == VIS.Team then return p.teamId == teamId end + if vis == VIS.Squad then return p.teamId == teamId and p.squadId == squadId end + return true +end + +-- ASCII sanitizer (curly quotes / dashes / ellipsis -> plain) +local function sanitize_ascii(s) + if not s then return "" end + s = tostring(s) + s = s:gsub("\226\128\153", "'"):gsub("\226\128\156", "\""):gsub("\226\128\157", "\"") + :gsub("\226\128\166", "..."):gsub("\226\128[\145\146\147\148]", "-") + :gsub("[^\32-\126]", "") + return s +end + +-- Strip leading tags like [AU], (XX), {YY} +local function strip_tags(name) + if not name then return "" end + return (name:gsub("^%s*%b[]", "") + :gsub("^%s*%b()", "") + :gsub("^%s*%b{}", "")):gsub("^%s+", "") +end + +-- Case-insensitive "does text contain player's (tag-stripped) name?" +local function mentions_me(text) + local p = me() + if not p or not p.name or not text then return false end + local mine = strip_tags(p.name):lower() + if mine == "" then return false end + return text:lower():find(mine, 1, true) ~= nil +end + +NetEvents:Subscribe('BotChatter:Say', function(botName, text, vis, teamId, squadId) + if not visible_to_me(vis, teamId, squadId) then return end + local chan = (vis == VIS.Team and "TEAM") or (vis == VIS.Squad and "SQUAD") or "ALL" + + messages[#messages+1] = { + name = sanitize_ascii(botName or "BOT"), + text = sanitize_ascii(text or ""), + chan = chan, + vis = vis, + teamId = teamId, + squadId= squadId, + t = now(), + mention= mentions_me(text) + } + if #messages > MAX_LINES then table.remove(messages, 1) end +end) + +Events:Subscribe('Level:Loaded', function() messages = {} end) +Events:Subscribe('Level:Destroy', function() messages = {} end) + +-- --------------- UI tuning --------------- +local function win() return ClientUtils:GetWindowSize() end + +-- Placement (top-left) +local PAD_LEFT_RATIO = 0.05 +local PAD_TOP_RATIO = 0.05 + +-- Base sizing (we'll adapt per frame) +local LINE_PX_BASE = 18 +local FONT_SCALE_BASE = 1.0 + +-- Limit & fade +MAX_LINES = 10 +local LIFETIME = 10.0 +local FADE_START = 7.0 + +-- Colors (RGBA 0..1). BF3-ish: Team1 ~ blue, Team2 ~ orange. +local COLOR_TAG_ALL = Vec4(0.61, 0.82, 1.00, 0.95) +local COLOR_TAG_TEAM = Vec4(0.49, 1.00, 0.61, 0.95) +local COLOR_TAG_SQUAD = Vec4(1.00, 0.87, 0.48, 0.95) + +local NAME_TEAM1 = Vec4(0.75, 0.85, 1.00, 0.98) +local NAME_TEAM2 = Vec4(1.00, 0.80, 0.55, 0.98) +local NAME_NEUTRAL = Vec4(1.00, 1.00, 1.00, 0.98) + +local TEXT_TEAM1 = Vec4(0.82, 0.90, 1.00, 0.96) +local TEXT_TEAM2 = Vec4(1.00, 0.88, 0.70, 0.96) +local TEXT_GLOBAL = Vec4(0.90, 0.90, 0.90, 0.96) +local TEXT_SQUAD = Vec4(0.85, 1.00, 0.78, 0.96) + +local MENTION_HILITE = Vec4(1.00, 0.95, 0.40, 1.00) -- bright yellow for mentions +local SHADOW = Vec4(0, 0, 0, 0.85) + +-- Char width estimate (pixels at scale=1). Keep integer math for crispness. +local CHAR_PX = 8 + +-- --------------- Drawing --------------- +Events:Subscribe('UI:DrawHud', function() + if #messages == 0 then return end + + local res = win() + + -- Adaptive scale: proportionally bigger on high res, clamp for legibility + local scale = FONT_SCALE_BASE * math.min(1.5, math.max(0.95, (res.y / 1080.0) * 1.05)) + local step = math.floor(LINE_PX_BASE * scale + 0.5) + local baseX = math.floor(res.x * PAD_LEFT_RATIO + 0.5) + local baseY = math.floor(res.y * PAD_TOP_RATIO + 0.5) + local tnow = now() + + local function apply_a(v, a) return Vec4(v.x, v.y, v.z, v.w * a) end + + local y = baseY + for i = #messages, 1, -1 do + local m = messages[i] + local age = tnow - m.t + if age > LIFETIME then + table.remove(messages, i) + else + -- fade + local fade = 1.0 + if age > FADE_START then + local k = math.min(1.0, (age - FADE_START) / (LIFETIME - FADE_START)) + fade = 1.0 - 0.75 * k + end + + local tagStr = "[" .. m.chan .. "] " + local nameStr = m.name .. ": " + local textStr = m.text + + -- Choose colors + local tagColor = + (m.chan == "TEAM" and COLOR_TAG_TEAM) or + (m.chan == "SQUAD" and COLOR_TAG_SQUAD) or + COLOR_TAG_ALL + + local nameColor = + (m.teamId == TeamId.Team1 and NAME_TEAM1) or + (m.teamId == TeamId.Team2 and NAME_TEAM2) or + NAME_NEUTRAL + + local baseText = + (m.vis == VIS.Squad and TEXT_SQUAD) or + (m.teamId == TeamId.Team1 and TEXT_TEAM1) or + (m.teamId == TeamId.Team2 and TEXT_TEAM2) or + TEXT_GLOBAL + + -- Mention highlight gets priority + local textColor = m.mention and MENTION_HILITE or baseText + + -- Apply fade + local tagC = apply_a(tagColor, fade) + local nmC = apply_a(nameColor, fade) + local txC = apply_a(textColor, fade) + local shC = apply_a(SHADOW, fade) + + -- Left-aligned segments with integer spacing + local char_px_scaled = math.floor(CHAR_PX * scale + 0.5) + local x_tag = baseX + local x_name = x_tag + (#tagStr * char_px_scaled) + local x_text = x_name + (#nameStr * char_px_scaled) + + -- Subtle speaker cue arrow for SQUAD or MENTION + local prefix = (m.mention and "▶ ") or (m.vis == VIS.Squad and "› " or "") + if prefix ~= "" then + tagStr = prefix .. tagStr + x_name = x_name + (#prefix * char_px_scaled) + x_text = x_text + (#prefix * char_px_scaled) + end + + -- shadow (+1 px) + DebugRenderer:DrawText2D(x_tag + 1, y + 1, tagStr, shC, scale) + DebugRenderer:DrawText2D(x_name + 1, y + 1, nameStr, shC, scale) + DebugRenderer:DrawText2D(x_text + 1, y + 1, textStr, shC, scale) + + -- main + DebugRenderer:DrawText2D(x_tag, y, tagStr, tagC, scale) + DebugRenderer:DrawText2D(x_name, y, nameStr, nmC, scale) + DebugRenderer:DrawText2D(x_text, y, textStr, txC, scale) + + y = y + step + if ((y - baseY) / step) >= MAX_LINES then break end + end + end +end) diff --git a/ext/Client/__init__.lua b/ext/Client/__init__.lua index 7504a946..74f3bfd7 100644 --- a/ext/Client/__init__.lua +++ b/ext/Client/__init__.lua @@ -27,6 +27,10 @@ require('__shared/WeaponList') require('__shared/EbxEditUtils') require('__shared/Utils/Logger') +-- load the chat hooks +require('BotChatterClient') +-- require('BotChatterUI') -- deprecated + ---@type Logger local m_Logger = Logger("FunBotClient", Debug.Client.INFO) diff --git a/ext/Server/BotChatter.lua b/ext/Server/BotChatter.lua new file mode 100644 index 00000000..c2a4f345 --- /dev/null +++ b/ext/Server/BotChatter.lua @@ -0,0 +1,474 @@ +-- ext/Server/BotChatter.lua +-- Code by: JMDigital (https://github.com/JenkinsTR) +-- Core server logic for modular bot chatter +-- Packs, personalities, distortion & config are under: +-- __shared/BotChatterConfig.lua +-- ext/Server/BotChatter/{PackLoader.lua, Personalities.lua, Distort.lua, Util.lua, Packs/*.lua} + +local rnd = math.random + +-- ====== Module imports ====== +local ChatCfg = require('__shared/BotChatterConfig') +local PackLoader = require('BotChatter/PackLoader') +local Personalities = require('BotChatter/Personalities') +local Distort = require('BotChatter/Distort') +local Util = require('BotChatter/Util') + +-- Try to read BOT_TOKEN (for bot detection fallback) +local BOT_TOKEN = "" +pcall(function() + local ok, reg = pcall(require, '__shared/Registry/Registry') + if ok and reg and reg.COMMON and reg.COMMON.BOT_TOKEN ~= nil then + BOT_TOKEN = reg.COMMON.BOT_TOKEN or "" + end +end) + +-- ====== Local state ====== +local ActiveDefaultPack = PackLoader.Load(ChatCfg.defaultPack) +local LoadedPacks = { [ActiveDefaultPack.id] = ActiveDefaultPack } + +local levelLoaded = false +local roundStartDone = false + +-- Kill/multi/streak/revenge tracking +local lastSpeak = {} -- [botName] = { onKill=ts, onDeath=ts, onSpawn=ts, onSpecial=ts } +local lastKillTime = {} -- [botName] = timestamp of last kill +local multiCount = {} -- [botName] = multi count within window +local streakCount = {} -- [botName] = kills since last death +local lastKillerOf = {} -- [victimName] = killerName (for Revenge detection) +local Rate = {} -- per-bot spam control: [botName] = { times = {t1,t2,...} } + +-- ====== Utility ====== +local function now() return SharedUtils:GetTime() end +local function pick(tbl) return tbl[rnd(#tbl)] end + +local VIS = { Global = 0, Team = 1, Squad = 2 } + +local function visibilityKeyToEnum(key) + if key == "team" then return VIS.Team end + if key == "squad" then return VIS.Squad end + return VIS.Global +end + +local function isBot(p) + if p == nil then return false end + if p.onlineId == 0 then return true end -- Fun-Bots signature + if p.isBot == true then return true end + if BOT_TOKEN ~= "" and p.name then + if string.sub(p.name, 1, #BOT_TOKEN) == BOT_TOKEN then + return true + end + end + return false +end + +local function getPackById(id) + if LoadedPacks[id] then return LoadedPacks[id] end + local p = PackLoader.Load(id) + LoadedPacks[p.id] = p + return p +end + +local _packCache = {} +local function packForName(name) + if not ChatCfg.allowPerBotPackByTag then return ActiveDefaultPack end + if _packCache[name] then return _packCache[name] end + local upper = (name or ""):upper() + for tag, packId in pairs(ChatCfg.tagToPack) do + if upper:find(tag:upper(), 1, true) == 1 then + local p = getPackById(packId) + _packCache[name] = p + return p + end + end + _packCache[name] = ActiveDefaultPack + return ActiveDefaultPack +end + +-- Personality selection +local personas = { "Chill", "Cocky", "Tactical", "Sassy" } +local personaFor = {} + +local function pickPersonality(botName) + local pack = packForName(botName) + local bias = (pack and pack.PersonalityBias) or {} + + if ChatCfg.personalityMode == "single" then + return ChatCfg.forcedPersonality or "Tactical" + end + + if ChatCfg.personalityMode == "seeded" then + if not personaFor[botName] then + -- build weighted list once per bot, using bias + local weights = { Chill = 1, Cocky = 1, Tactical = 1, Sassy = 1 } + for k,v in pairs(bias) do weights[k] = math.max(0.01, (weights[k] or 1) * v) end + -- deterministic pick + local h = Util.simple_hash(botName or "BOT", ChatCfg.seed or 1337) + local total = (weights.Chill or 0) + (weights.Cocky or 0) + (weights.Tactical or 0) + (weights.Sassy or 0) + local roll = (h % 10000) / 10000 * total + local acc = 0 + for _,key in ipairs({"Chill","Tactical","Cocky","Sassy"}) do + acc = acc + (weights[key] or 0) + if roll <= acc then personaFor[botName] = key; break end + end + personaFor[botName] = personaFor[botName] or "Tactical" + end + return personaFor[botName] + end + + -- randomEachEvent with bias + local weights = { Chill = 1, Cocky = 1, Tactical = 1, Sassy = 1 } + for k,v in pairs(bias) do weights[k] = math.max(0.01, (weights[k] or 1) * v) end + local total = 0; for _,k in ipairs({"Chill","Tactical","Cocky","Sassy"}) do total = total + (weights[k] or 0) end + local roll = math.random() * total; local acc = 0 + for _,k in ipairs({"Chill","Tactical","Cocky","Sassy"}) do acc = acc + (weights[k] or 0); if roll <= acc then return k end end + return "Tactical" +end + +-- Rate-limit per bot (anti-spam) +local function rateAllow(botName) + local conf = ChatCfg.rateLimit or { windowSec = 8, maxPerWindow = 2 } + local win = conf.windowSec or 8 + local maxN = conf.maxPerWindow or 2 + local tnow = now() + + Rate[botName] = Rate[botName] or { times = {} } + local times = Rate[botName].times + + -- prune old + local j = 1 + for i=1,#times do if tnow - times[i] <= win then times[j] = times[i]; j = j + 1 end end + for k=j,#times do times[k] = nil end + + if #times >= maxN then return false end + times[#times+1] = tnow + return true +end + +-- Cooldown per category (legacy, per-bot) +local function canSpeak(name, category) + if not name then return false end + lastSpeak[name] = lastSpeak[name] or {} + local cdMap = { + onKill = 10, + onDeath = 12, + onSpawn = 20, + onSpecial = 8 + } + local cd = cdMap[category] or cdMap.onSpecial + local last = lastSpeak[name][category] or -1e9 + if now() - last >= cd then + lastSpeak[name][category] = now() + return true + end + return false +end + +-- Line selection (pack + personality + optional named variants) +local function chooseFrom(category, botName, enemyName, preferNamed) + local pack = packForName(botName) + local persona = pickPersonality(botName) + + local base = (pack.Lines and pack.Lines[category]) or (ActiveDefaultPack.Lines and ActiveDefaultPack.Lines[category]) or {} + local personaL = (Personalities[persona] and Personalities[persona][category]) or {} + + local namedText = nil + if preferNamed and enemyName then + local namedKey = category .. "Named" + local namedBase = (pack.Lines and pack.Lines[namedKey]) or (ActiveDefaultPack.Lines and ActiveDefaultPack.Lines[namedKey]) + if namedBase and #namedBase > 0 and rnd() < 0.33 then + namedText = pick(namedBase):gsub("{enemy}", enemyName) + end + end + + if namedText then + return namedText, pack + end + + local pool = Util.merge_arrays(base, personaL, nil) + if #pool == 0 then return nil, pack end + return pick(pool), pack +end + +-- Distortion + casing +local function finalizeLine(raw, pack) + raw = raw or "" + local casing = (pack.Tweaks and pack.Tweaks.casing) or (ActiveDefaultPack.Tweaks and ActiveDefaultPack.Tweaks.casing) or "lower" + if casing == "lower" then raw = raw:lower() end + raw = Distort.apply(raw, ChatCfg.distort, pack.Tweaks) + return raw +end + +local function humanName(p) + if not p then return "" end + return p.name or "" +end + +-- Vehicle kill heuristic +local function isVehicleKill(weapon, roadKill) + if roadKill then return true end + local lw = string.lower(tostring(weapon or "")) + local hints = { + "tank","mbt","ifv","lav","apc","buggy","jeep","vodnik","humvee","boat", + "jet","f18","f-18","su-","mig","flanker","hornet", + "heli","helicopter","ah-","ka-","z-11","viper","havoc","littlebird","scout", + "stationary","aa","igla","stinger" + } + for _,h in ipairs(hints) do + if string.find(lw, h, 1, true) then return true end + end + return false +end + +-- Longshot (distance) check - robust & VU-friendly +local function isLongshot(inflictor, victimPos) + -- Guard: VU gives a Vec3 here; ensure it's sane + if not victimPos or victimPos.x == nil then + return false + end + + -- Try to get the killer's current world position. + -- On server, soldier is the safest source. + local pos = nil + local ok = pcall(function() + if inflictor ~= nil and inflictor.soldier ~= nil and inflictor.soldier.worldTransform ~= nil then + pos = inflictor.soldier.worldTransform.trans -- Vec3 + end + end) + if not ok or pos == nil then + return false + end + + -- Vec3 has no :Length(); use .magnitude or :Distance() + local d = (pos - victimPos).magnitude + return d >= 80.0 -- ~80m threshold +end + +-- tiny one-shot timer +local _Timers = {} +local function Timer(sec, fn) table.insert(_Timers, {t = now() + sec, fn = fn}) end +Events:Subscribe('Engine:Update', function() + if #_Timers == 0 then return end + local t = now() + for i = #_Timers, 1, -1 do + if t >= _Timers[i].t then + local fn = _Timers[i].fn + table.remove(_Timers, i) + pcall(fn) + end + end +end) + +-- reset per-round state helper to avoid drift +local function resetRoundState() + lastSpeak, lastKillTime, multiCount, streakCount, lastKillerOf, Rate = {}, {}, {}, {}, {}, {} + _packCache, personaFor = {}, {} + roundStartDone = false +end + +-- ====== Server->Client emitter ====== +local function sendLine(text, speakerPlayer, visKey) + local name = "SERVER" + local teamId = TeamId.Team1 + local squadId = SquadId.SquadNone + if speakerPlayer and speakerPlayer.name then name = speakerPlayer.name end + if speakerPlayer and speakerPlayer.teamId ~= nil then teamId = speakerPlayer.teamId end + if speakerPlayer and speakerPlayer.squadId ~= nil then squadId = speakerPlayer.squadId end + local vis = visibilityKeyToEnum(visKey or "global") + NetEvents:BroadcastUnreliable('BotChatter:Say', name, text, vis, teamId, squadId) +end + +-- ====== Round lifecycle ====== +Events:Subscribe('Extension:Loaded', function() + math.randomseed(os.time() % 2147483647) +end) + +Events:Subscribe('Level:Loaded', function() + levelLoaded = true + resetRoundState() + + -- Round start: try a few times to find any bot so we can attribute the line + local tries = 8 + for i = 1, tries do + Timer(i, function() + if not levelLoaded or roundStartDone then return end + local all = PlayerManager:GetPlayers() + local speaker + for _, p in ipairs(all) do if isBot(p) then speaker = p; break end end + if speaker then + local botName = humanName(speaker) + if rateAllow(botName) and canSpeak(botName, 'onSpecial') then + local line, pack = chooseFrom('RoundStartGlobal', botName, nil, false) + if line then + sendLine(finalizeLine(line, pack), speaker, "global") + roundStartDone = true + end + end + end + end) + end +end) + +Events:Subscribe('Level:Destroy', function() + if not levelLoaded then return end + levelLoaded = false + + -- Round end + local all = PlayerManager:GetPlayers() + local speaker = nil + for _, p in ipairs(all) do if isBot(p) then speaker = p; break end end + if speaker then + local botName = humanName(speaker) + if rateAllow(botName) and canSpeak(botName, 'onSpecial') then + local line, pack = chooseFrom('RoundEndGlobal', botName, nil, false) + if line then sendLine(finalizeLine(line, pack), speaker, "global") end + end + end + resetRoundState() +end) + +-- ====== Events ====== +-- Spawn +Events:Subscribe('Player:Respawn', function(player) + if not levelLoaded or not isBot(player) then return end + local botName = humanName(player) + if rateAllow(botName) and canSpeak(botName, 'onSpawn') then + local line, pack = chooseFrom('Spawn', botName, nil, false) + if line then sendLine(finalizeLine(line, pack), player, "team") end + end +end) + +-- Vehicle enter/exit (light noise, rate-limited by onSpecial) +Events:Subscribe('Vehicle:Enter', function(vehicle, player) + if not levelLoaded or not isBot(player) then return end + local botName = humanName(player) + if rateAllow(botName) and canSpeak(botName, 'onSpecial') then + local line, pack = chooseFrom('VehEnter', botName, nil, false) + if line then sendLine(finalizeLine(line, pack), player, "team") end + end +end) + +Events:Subscribe('Vehicle:Exit', function(vehicle, player) + if not levelLoaded or not isBot(player) then return end + local botName = humanName(player) + if rateAllow(botName) and canSpeak(botName, 'onSpecial') then + local line, pack = chooseFrom('VehExit', botName, nil, false) + if line then sendLine(finalizeLine(line, pack), player, "team") end + end +end) + +-- Kills +Events:Subscribe('Player:Killed', +function(victim, inflictor, position, weapon, roadKill, headshot, victimInRevive) + if not levelLoaded then return end + + -- Track last killer for revenge + if victim and inflictor then + lastKillerOf[humanName(victim)] = humanName(inflictor) + end + + -- Reset victim streak if victim is bot + if isBot(victim) and victim.name then + streakCount[victim.name] = 0 + end + + -- Only emit chatter for bot killer + if not isBot(inflictor) then return end + local killer = inflictor + local knameHuman = humanName(killer) -- shown as speaker (keeps tag) + local victimHuman = humanName(victim) -- shown if needed (keeps tag) + local victimMention = Util.strip_tags(victimHuman) -- used in {enemy} substitutions + local wpn = tostring(weapon or "") + local tnow = now() + + -- Anti-spam guard first + if not rateAllow(knameHuman) then return end + + -- Multi-kill tracking + local isMulti = false + multiCount[knameHuman] = multiCount[knameHuman] or 0 + if lastKillTime[knameHuman] and (tnow - lastKillTime[knameHuman] <= 6.0) then + multiCount[knameHuman] = multiCount[knameHuman] + 1 + isMulti = (multiCount[knameHuman] >= 2) + else + multiCount[knameHuman] = 1 + end + lastKillTime[knameHuman] = tnow + + -- Streak tracking + streakCount[knameHuman] = (streakCount[knameHuman] or 0) + 1 + local hitStreak = (streakCount[knameHuman] == 6) + + local said = false + + -- Priority 1: multi-kill + if isMulti and canSpeak(knameHuman, 'onSpecial') then + local cat = (multiCount[knameHuman] >= 4) and 'Multi4' or (multiCount[knameHuman] == 3 and 'Multi3' or 'Multi2') + local line, pack = chooseFrom(cat, knameHuman, victimMention, false) + if line then + sendLine(finalizeLine(line, pack), killer, "global") + said = true + end + end + + -- Priority 2: revenge (if victim previously killed this bot recently) + if not said and victim and victim.name then + local killerOfMe = lastKillerOf[knameHuman] + if killerOfMe and killerOfMe == victimHuman and canSpeak(knameHuman, 'onSpecial') then + local line, pack = chooseFrom('Revenge', knameHuman, victimMention, true) + if line then + sendLine(finalizeLine(line, pack), killer, "global") + said = true + end + end + end + + -- Priority 3: headshot + if not said and headshot and canSpeak(knameHuman, 'onSpecial') then + local long = isLongshot(killer, position) + local cat = long and 'Longshot' or 'Headshot' + local line, pack = chooseFrom(cat, knameHuman, victimMention, true) + if line then + sendLine(finalizeLine(line, pack), killer, "global") + said = true + end + end + + -- Priority 4: vehicle/roadkill + if not said and isVehicleKill(weapon, roadKill) and canSpeak(knameHuman, 'onSpecial') then + local cat = roadKill and 'Roadkill' or 'VehicleKill' + local line, pack = chooseFrom(cat, knameHuman, victimMention, true) + if line then + sendLine(finalizeLine(line, pack), killer, "global") + said = true + end + end + + -- Priority 5: streak hype (doesn't block normal kill) + if hitStreak and canSpeak(knameHuman, 'onSpecial') then + local line, pack = chooseFrom('Streak', knameHuman, nil, false) + if line then + sendLine(finalizeLine(line, pack), killer, "global") + -- (no 'said = true'; allow regular kill line too) + end + end + + -- Fallback: generic kill (occasionally named) + if canSpeak(knameHuman, 'onKill') then + local line, pack = chooseFrom('Kill', knameHuman, victimMention, true) + if line then + sendLine(finalizeLine(line, pack), killer, "global") + end + end +end) + +-- Optional simple command to switch default pack at runtime: !bcpack PackId +Events:Subscribe('Player:Chat', function(p, mask, msg) + msg = tostring(msg or "") + if msg:sub(1,8) == "!bcpack " then + local want = msg:sub(9):gsub("%s+$","") + ActiveDefaultPack = PackLoader.Reload(want) + LoadedPacks = { [ActiveDefaultPack.id] = ActiveDefaultPack } -- reset cache + ChatManager:Yell("BotChatter pack: " .. ActiveDefaultPack.id, 3.0) + end +end) diff --git a/ext/Server/BotChatter/Distort.lua b/ext/Server/BotChatter/Distort.lua new file mode 100644 index 00000000..6a116534 --- /dev/null +++ b/ext/Server/BotChatter/Distort.lua @@ -0,0 +1,64 @@ +-- ext/Server/BotChatter/Distort.lua +-- Code by: JMDigital (https://github.com/JenkinsTR) +local M = {} + +local emoticons = { ":)", ";)", ":D", ":P", ">:)", "xD", ":^)", ":3" } +local vowels = { a=true, e=true, i=true, o=true, u=true } + +local function rand(tbl) return tbl[math.random(#tbl)] end + +local function sanitize_ascii(s) + -- Replace known curly quotes/dashes with ASCII + s = s:gsub("[\226\128\153\226\128\156\226\128\157]", "'") -- ’ “ ” + s = s:gsub("[\226\128\147\226\128\148]", "--") -- en/em-dash -> -- + -- Remove other non-ASCII + s = s:gsub("[^\x20-\x7E]", "") + return s +end + +local function elongate(s, maxTimes) + if maxTimes <= 0 then return s end + -- elongate last vowel in a random word + local words = {} + for w in s:gmatch("%S+") do table.insert(words, w) end + if #words == 0 then return s end + local idx = math.random(#words) + local w = words[idx] + local pos + for i = #w,1,-1 do + local ch = w:sub(i,i):lower() + if vowels[ch] then pos = i; break end + end + if not pos then return s end + local times = math.random(1, maxTimes) + words[idx] = w:sub(1,pos) .. string.rep(w:sub(pos,pos), times) .. w:sub(pos+1) + return table.concat(words, " ") +end + +local function uppercase_burst(s) + -- randomly uppercase a short segment + if #s < 6 then return s:upper() end + local a = math.random(1, math.max(1, #s-3)) + local b = math.min(#s, a + math.random(2,5)) + return s:sub(1,a-1) .. s:sub(a,b):upper() .. s:sub(b+1) +end + +function M.apply(text, globalCfg, packTweaks) + text = sanitize_ascii(text or "") + local cfg = globalCfg or {} + local tw = (packTweaks and packTweaks.distort) or {} + + local emot = (tw.emoticonChance or cfg.emoticonChance or 0) + local elong= (tw.elongateChance or cfg.elongateChance or 0) + local up = (tw.uppercaseBurstChance or cfg.uppercaseBurstChance or 0) + local maxE = (tw.maxElongate or cfg.maxElongate or 2) + + if cfg.enabled then + if math.random() < elong then text = elongate(text, maxE) end + if math.random() < up then text = uppercase_burst(text) end + if math.random() < emot then text = text .. " " .. rand(emoticons) end + end + return text +end + +return M diff --git a/ext/Server/BotChatter/PackLoader.lua b/ext/Server/BotChatter/PackLoader.lua new file mode 100644 index 00000000..f4adbb52 --- /dev/null +++ b/ext/Server/BotChatter/PackLoader.lua @@ -0,0 +1,30 @@ +-- ext/Server/BotChatter/PackLoader.lua +-- Code by: JMDigital (https://github.com/JenkinsTR) +local M = {} + +local function safe_require(path) + local ok, mod = pcall(require, path) + if ok then return mod end + print("[BotChatter] require failed: " .. path .. " -> " .. tostring(mod)) + return nil +end + +function M.Load(name) + local mod = safe_require('BotChatter/Packs/' .. name) + if not mod then + print("[BotChatter] Falling back to Default pack.") + mod = safe_require('BotChatter/Packs/Default') or { Lines = {}, Aliases = {} } + end + -- normalize empty tables + mod.Lines = mod.Lines or {} + mod.PersonalityBias = mod.PersonalityBias or {} -- optional per-pack bias weights + mod.Tweaks = mod.Tweaks or {} -- optional distortion tweaks, casing, etc. + return mod +end + +function M.Reload(name) + package.loaded['BotChatter/Packs/' .. name] = nil + return M.Load(name) +end + +return M diff --git a/ext/Server/BotChatter/Packs/AU.lua b/ext/Server/BotChatter/Packs/AU.lua new file mode 100644 index 00000000..9253379a --- /dev/null +++ b/ext/Server/BotChatter/Packs/AU.lua @@ -0,0 +1,64 @@ +-- ext/Server/BotChatter/Packs/AU.lua +-- Code by: JMDigital (https://github.com/JenkinsTR) +-- Light Aussie flavour; ASCII only; keep it subtle so it doesn't sound forced. +local AU = {} + +AU.Lines = { + Kill = { + "cop that.","too easy, mate.","righto.","beaut.","nice peel.", + "cheers for the peek.","on ya.","sorted.","no dramas.","done and dusted.", + "clean as.","mint angle.","walked into it.","that'll do.","easy pick.","job done." + }, + Death = { + "ah yep, fair.","got me good.","cop it back.","stiff one.","right in the pride.", + "yeah nah.","he earned that.","bit dusty, my bad.","timing cooked me.","deserved, tbh." + }, + Spawn = { + "back on, mate.","right behind ya.","we're on.","ok, send it.","warming up.", + "keen.","moving now.","eyes up.","reset and roll.","fresh mags." + }, + Headshot = { "clean as.","love that.","one click, mate.","bonza flick.","crisp as.","top notch." }, + Revenge = { "we're square, mate.","that's for before.","all even.","paid in full.","closed the loop." }, + Roadkill = { "mind the ute.","watch the bumper.","bit of hoonery there.","street sweeper.","road's shut." }, + RoundStartGlobal = { "gl hf","have a ripper.","keep it tidy.","no dramas.","play smart." }, + RoundEndGlobal = { "gg","good on ya.","that was decent.","cheers for the runs.","nice shift." }, + + VehicleKill = { + "armor cracked.","bird down.","vehicle disabled.","tank gone.","pilot's gone.","nice ride - gone.", + "tracks busted.","driver bailed.","aa down.","rotor stopped.","engine out.","free scrap." + }, + Multi2 = { "double.","two down.","back to back.","tempo up.","chain started.","keep feeding.","easy two.","momentum." }, + Multi3 = { "triple.","three piece.","they're falling apart.","on a tear.","lining up for me.","ok, who's next?","stack em." }, + Multi4 = { "multi on.","they can't stop me.","too many angles.","stack wiped.","send more, please.","getting rude now :)" }, + Streak = { "on a run.","untouchable.","they can't trade me.","farm mode.","heat check.","stacking bodies.","everything's clicking." }, + Longshot = { "long one.","don't peek at range.","i own that sightline.","distance diff.","postcard from the bush.","too comfy at range." }, + VehEnter = { "taking a ride.","mounting up.","i'm in.","driver ready.","gunning.","vroom." }, + VehExit = { "bailing.","hopping out.","on foot.","ditch the ride.","ground game now.","fresh air." }, + FirstBlood = { "first blood.","opening pick.","we start strong.","tempo set.","good start.","woke them up." }, + + -- Named variants + KillNamed = { + "gg {enemy}.","sit down, {enemy}.","trade denied, {enemy}.","peeked wrong, {enemy}.", + "outplayed, {enemy}.","see you at respawn, {enemy}.","angle was mine, {enemy}." + }, + HeadshotNamed = { + "one tap, {enemy}.","keep your head down, {enemy}.","peek punished, {enemy}.","clean head, {enemy}.","crispy on you, {enemy}." + }, + LongshotNamed = { "long one, {enemy}.","range diff, {enemy}.","eagle eye on you, {enemy}.","greetings from out back, {enemy}." }, + RevengeNamed = { "that's for earlier, {enemy}.","we're even, {enemy}.","payback delivered, {enemy}.","circle closed, {enemy}." }, + VehicleKillNamed = { "bye armor, {enemy}.","driver out, {enemy}.","bird down, {enemy}.","nice ride, {enemy}... was." }, + RoadkillNamed = { "mind the wheels, {enemy}.","hood ornament unlocked, {enemy}.","green light, {enemy}.","street pizza, {enemy}." }, + DeathNamed = { "nice shot, {enemy}.","ok you got me, {enemy}.","pre-aimed me, {enemy}.","good timing, {enemy}.","fair go, {enemy}." }, + + -- Weapon-specific + KnifeKill = { "shh.","silent op.","that was personal.","slice and dice.","backstab meta.","click-click shank." }, + GrenadeKill = { "cook perfect.","frag lands.","nade out.","catch.","boom timing.","bank shot." }, + ShotgunKill = { "close range diff.","12g says hi.","open the door.","point blank.","boomstick meta." }, + PistolKill = { "sidearm gaming.","pistol whipped.","secondary supremacy.","tap tap.","backup did it." }, + SniperKill = { "scope sings.","steady hands.","click at range.","lane owned.","glass cannon online." }, +} + +AU.Tweaks = { casing = "lower", distort = { emoticonChance = 0.12 } } +AU.PersonalityBias = { Chill = 1.2, Tactical = 1.0, Cocky = 0.9, Sassy = 0.9 } + +return { id = "AU", Lines = AU.Lines, Tweaks = AU.Tweaks, PersonalityBias = AU.PersonalityBias } diff --git a/ext/Server/BotChatter/Packs/CA.lua b/ext/Server/BotChatter/Packs/CA.lua new file mode 100644 index 00000000..568f99e4 --- /dev/null +++ b/ext/Server/BotChatter/Packs/CA.lua @@ -0,0 +1,63 @@ +-- ext/Server/BotChatter/Packs/CA.lua +-- Code by: JMDigital (https://github.com/JenkinsTR) +-- Canada flavour: clean, friendly, lightly hockey-coded. ASCII only; avoid overusing stereotypes. +local CA = {} + +CA.Lines = { + Kill = { + "beauty.","nice pick.","good one.","too clean.","dialed.","all good.", + "that'll do.","on to the next.","textbook.","clean work." + }, + Death = { + "oof.","good shot.","that's fair.","my bad.","alright then.","i'll get him back.", + "timing got me.","well taken.","respect on that." + }, + Spawn = { + "i'm in.","rolling.","let's go.","we move.","on you.","fresh mags.", + "all set.","we're good.","eyes up.","focus on." + }, + Headshot = { "top shelf.","one tap.","clean between the eyes.","crisp.","keep your head down." }, + Revenge = { "we're even.","call that settled.","that's for earlier.","paid back." }, + Roadkill = { "mind the truck.","road closed.","hood ornament unlocked.","drive-by diploma." }, + RoundStartGlobal = { "gl hf","let's have a good one.","play smart.","stay sharp." }, + RoundEndGlobal = { "gg","nice game.","good stuff.","cheers." }, + + VehicleKill = { + "armor cracked.","bird down.","vehicle disabled.","tank gone.","pilot out.", + "tracks busted.","driver bailed.","aa down.","rotor stopped.","engine out." + }, + Multi2 = { "double.","two down.","tempo up.","chain started.","easy two.","momentum." }, + Multi3 = { "triple.","three piece.","they're falling apart.","on a tear.","lining up." }, + Multi4 = { "multi on.","they can't stop me.","stack wiped.","send more, please." }, + Streak = { "on a run.","untouchable.","they can't trade me.","farm mode.","heat check." }, + Longshot = { "long one.","distance diff.","postcard from downtown.","laser at range.","lane mine." }, + VehEnter = { "taking a ride.","mounting up.","driver ready.","gunning.","moving." }, + VehExit = { "bailing.","hopping out.","on foot.","ground game.","fresh air." }, + FirstBlood = { "first blood.","opening pick.","we start strong.","tempo set." }, + + -- Named variants + KillNamed = { + "gg {enemy}.","sit down, {enemy}.","trade denied, {enemy}.","peeked wrong, {enemy}.", + "outplayed, {enemy}.","see you at respawn, {enemy}." + }, + HeadshotNamed = { + "one tap, {enemy}.","keep your head down, {enemy}.","peek punished, {enemy}.","clean head, {enemy}." + }, + LongshotNamed = { "long one, {enemy}.","range diff, {enemy}.","eagle eye on you, {enemy}." }, + RevengeNamed = { "we're even, {enemy}.","that settles it, {enemy}.","paid back, {enemy}." }, + VehicleKillNamed = { "bye armor, {enemy}.","driver out, {enemy}.","bird down, {enemy}." }, + RoadkillNamed = { "mind the wheels, {enemy}.","street pizza, {enemy}.","green light, {enemy}." }, + DeathNamed = { "nice shot, {enemy}.","you got me, {enemy}.","pre-aimed me, {enemy}.","good timing, {enemy}." }, + + -- Weapon-specific + KnifeKill = { "quiet now.","silent op.","that was personal.","slice and dice.","backstab meta." }, + GrenadeKill = { "cook perfect.","frag lands.","nade out.","catch.","boom timing.","bank shot." }, + ShotgunKill = { "close range diff.","12g says hello.","door's open.","point blank.","boomstick meta." }, + PistolKill = { "sidearm gaming.","pistol whipped.","secondary wins.","tap tap." }, + SniperKill = { "scope sings.","steady hands.","lane owned.","click at range." }, +} + +CA.Tweaks = { casing = "lower", distort = { emoticonChance = 0.10 } } +CA.PersonalityBias = { Chill = 1.1, Tactical = 1.0, Cocky = 0.95, Sassy = 1.0 } + +return { id = "CA", Lines = CA.Lines, Tweaks = CA.Tweaks, PersonalityBias = CA.PersonalityBias } diff --git a/ext/Server/BotChatter/Packs/Default.lua b/ext/Server/BotChatter/Packs/Default.lua new file mode 100644 index 00000000..c37bcd44 --- /dev/null +++ b/ext/Server/BotChatter/Packs/Default.lua @@ -0,0 +1,236 @@ +-- ext/Server/BotChatter/Packs/Default.lua +-- Code by: JMDigital (https://github.com/JenkinsTR) +local Lines = { + Kill = { + "got em.","down he goes.","clean shot.","too easy.","one less headache.","bye bye.","that'll do.", + "he never saw me.","boom.","confirmed.","next.","ez pick.","sit down.","outplayed.","deleted.", + "you peeked wrong.","catch you at respawn.","timing diff.","trade denied.","tagged and bagged.", + "held my angle.","you walked into that.","shoulda stayed home.","nice try tho.","night night.", + "thanks for the peek.","gg on that one.","that was free.","clean beams.","blink and he was gone.", + "i'll take that.","click heads, win games.","no refunds.","satisfying.","chef's kiss.", + "stacking bodies, sorry :/ ","too crisp.","that angle is mine.", + + -- more banter + "peek-a-boom.","map knowledge diff.","rotations > reactions.","caught window shopping.", + "rent free (for now).","montage material.","queue next, buddy.","you held w, i held angle.", + "out-rotated.","crosshair placement pays.","that timing was brutal.","spine reset.", + "respect the crossfire.","reading you like patch notes.","shoulda checked the mini-map.", + "aim lab paid rent.","hold that respawn screen.","clean transfer.","pre-aim wins.", + "thanks for the space.","out-macroed.","i'll sign for that delivery.","right place, right time.", + "late to the trade.","that jiggle wasn't enough.","free real estate.","surgical.", + }, + + Death = { + "ouch.","lucky shot...","they got me.","i'm hit.","welp.","next life i'm angry.", + "that stung.","i'll be back.","close one.","sniper somewhere.","oof.","respawning.", + "their angle was better.","got beamed.","walked right into that.","anyone trade him?", + "naded out.","pre-aimed me.","crossfired.","lost the 50/50.","they swung two.","timing cursed.", + "that hurt my pride more.","deserved tbh.","ok you win that.","i blinked.","hands cold, my bad.", + "nice shot.","he earned that one.","shoulda jiggle peeked.","i'm warmed now", + "note to self: never again.","i respect it.","yeah... that tracks.","gg on me.", + "mental reset.","deep breath.","clip it, idc.","friendly trade pls?", + + -- more banter + "fair play.","skill issue (mine).","good hold from them.","shoulda smoked that.", + "i forgot the mini-map existed.","i re-peeked like a bot.","timing tax paid.", + "stared at the radar too long.","walked into utility.","spacing was scuffed.", + "that crosshair was parked on me.","they earned that clear.","reset brain, try again.", + }, + + Spawn = { + "i'm in.","moving.","let's roll.","rotating.","regrouping.","back in.","on you.","flanking.","holding.", + "rally.","rolling out.","swinging wide.","i'm up.","coming in hot.","resetting.","cover me, i'm here.", + "hi again :)","fresh mags.","ok, focus.","we good.","small steps.","checking corners.","eyes open.", + "hands warm now.","head in the game.","alright, fun time.", + + -- more banter + "same plan, better aim.","let's tidy this up.","play for trades.","work the crossfires.", + "utility first, then faces.","i'll entry, trade me.","holding space.","defaulting first.", + "slow it down, read info.","no hero plays, just trades.", + }, + + Headshot = { + "head clean.","right between the eyes.","hold still next time.","peek punished.","don't peek me.", + "nice helmet, didn't help.","one tap.","click.","keep your head down.","crosshair diff.","that was crispy.", + "blame your ping.","cold flick.","pre-aim gods smile.","eyes closed (jk).","tight angle, tighter shot.", + "that was mean :)","goodnight forehead.", + + -- more banter + "mind the melon.","cranial clearance approved.","forehead check passed.","scope said yes.", + "micro-adjust landed.","that line was pre-paid.","taxed the noggin.", + }, + + VehicleKill = { + "armor cracked.","bird down.","vehicle disabled.","tank gone.","pilot's gone.","nice ride - gone.", + "tracks busted.","driver bailed.","aa down.","rotor stopped.","engine out.","bye bye armor.", + "that dash light is permanent.","smoking... not in a good way.","free scrap metal.","warranty voided.", + + -- more banter + "tow bill's on me.","control-alt-delete on that chassis.","return to sender.", + "lost visual, lost vehicle.","maintenance required: everything.", + }, + + Multi2 = { + "double.","two down.","back to back.","tempo up.","chain started.","keep feeding.","snowball rolling.","easy 2.","momentum.", + + -- more banter + "duo discounted.","two-piece special.","buy one, tag one.", + }, + + Multi3 = { + "triple.","three piece.","they're falling apart.","keep feeding.","on a tear.","they're lining up.","3 clean.","ok, who's next?","stack em.", + + -- more banter + "trifecta online.","queue the music.","that escalated nicely.", + }, + + Multi4 = { + "multi on.","they can't stop me.","too many angles.","stack wiped.","uninstall vibes.","they keep peeking.","send more, please.","this is getting rude :)", + + -- more banter + "lobby control obtained.","that's a highlight reel.", + }, + + Streak = { + "on a run.","untouchable.","they can't trade me.","farm mode.","heat check.","stacking bodies.","pacing the lobby.","winning time.","snowball secured.","everything's clicking.", + + -- more banter + "heater.","hands toasty.","momentum secured.","confidence online.", + }, + + Roadkill = { + "free uber.","bumper bonus.","tire marks say hi.","mind the wheels.","oops, hood ornament.","road's closed.","drive-by diploma.","vroom vroom, bye.", + + -- more banter + "pavement patrol.","traffic calming measure applied.", + }, + + Revenge = { + "that's for earlier.","we're even.","payback delivered.","told you i'd be back.","balance restored.","debt paid.","remember me?","circle closed.","all squared up.", + + -- more banter + "ledger balanced.","bookmark removed.","closure achieved.", + }, + + Longshot = { + "long one.","don't peek at range.","i own that sightline.","call me eagle eye.","distance diff.","scope singing.","postcard from downtown.","too comfy at range.", + + -- more banter + "signed from downtown.","wind checked, shot sent.","lane belongs to me.", + }, + + VehEnter = { + "taking a ride.","mounting up.","i'm in.","driver ready.","gunning.","shotgun seat.","vroom.","mobile now.", + + -- more banter + "keys acquired.","ride online.", + }, + + VehExit = { + "bailing.","hopping out.","out.","on foot.","ditch the ride.","ground game now.","parking here.","fresh air.","legs online.", + + -- more banter + "handbrake on.","tires cooled.", + }, + + FirstBlood = { + "first blood.","opening pick.","we start strong.","tempo set.","good start.","that wakes them up.","hello momentum.", + + -- more banter + "door opened.","table set.","tone established.", + }, + + -- Named variants (use {enemy}) + KillNamed = { + "gg {enemy}.","sit down, {enemy}.","tagged you, {enemy}.","trade denied, {enemy}.", + "peeked wrong, {enemy}.","walked into it, {enemy}.","outplayed, {enemy}.", + "see you at respawn, {enemy}.","that angle was mine, {enemy}.","nice try, {enemy}.", + + -- more banter + "timing diff on you, {enemy}.","map read you, {enemy}.","pre-aim landed, {enemy}.", + "caught you switching, {enemy}.","clean punish, {enemy}.", + }, + + HeadshotNamed = { + "one tap, {enemy}.","keep your head down, {enemy}.","peek punished, {enemy}.", + "clean head, {enemy}.","crispy on you, {enemy}.","shoulda ducked, {enemy}.", + + -- more banter + "forehead tax, {enemy}.","mind the melon, {enemy}.", + }, + + LongshotNamed = { + "long one, {enemy}.","range diff, {enemy}.","eagle eye on you, {enemy}.","greetings from far away, {enemy}.", + + -- more banter + "postcard delivered, {enemy}.","scope said yes, {enemy}.", + }, + + RevengeNamed = { + "that's for earlier, {enemy}.","we're even, {enemy}.","payback delivered, {enemy}.","circle closed, {enemy}.", + + -- more banter + "ledger balanced, {enemy}.","tab closed, {enemy}.", + }, + + VehicleKillNamed = { + "bye armor, {enemy}.","driver out, {enemy}.","bird down, {enemy}.","nice ride, {enemy}... was.", + + -- more banter + "tow truck for you, {enemy}.","service light on, {enemy}.", + }, + + RoadkillNamed = { + "mind the wheels, {enemy}.","street pizza, {enemy}.","hood ornament unlocked, {enemy}.","green light, {enemy}.", + + -- more banter + "crosswalk denied, {enemy}.","traffic stop, {enemy}.", + }, + + DeathNamed = { + "nice shot, {enemy}.","ok you got me, {enemy}.","pre-aimed me, {enemy}.","good timing, {enemy}.","alright fair, {enemy}.","respect, {enemy}.", + + -- more banter + "clean clear, {enemy}.","earned it, {enemy}.","you read me, {enemy}.", + }, + + -- Weapon-specific + KnifeKill = { "shh.","silent op.","that was personal.","slice and dice.","backstab meta.","click-click shank.", + -- more banter + "quiet paperwork.","sharp decisions.", + }, + GrenadeKill = { "cook perfect.","frag lands.","nade out.","catch.","boom timing.","bank shot.", + -- more banter + "delivered with a timer.","package signed.", + }, + ShotgunKill = { "close range diff.","12g says hi.","open the door.","point blank.","boomstick meta.", + -- more banter + "door breacher moment.","knock-knock resolved.", + }, + PistolKill = { "sidearm gaming.","pistol whipped.","secondary supremacy.","tap tap.","backup did it.", + -- more banter + "budget aim, premium result.","sidearm clutch.", + }, + SniperKill = { "glass cannon online.","scope sings.","steady hands.","click at range.","lane owned.", + -- more banter + "breath held, shot sent.","threaded the needle.", + }, +} + +return { + id = "Default", + Lines = Lines, + + -- Optional: pack-level tweaks (merged with global config) + Tweaks = { + casing = "lower", -- "lower" | "asis" + distort = { + emoticonChance = 0.10 + }, + }, + + -- Optional: bias personalities for this pack (weights) + PersonalityBias = { + Chill = 1.0, Cocky = 1.0, Tactical = 1.0, Sassy = 1.0 + } +} diff --git a/ext/Server/BotChatter/Packs/NZ.lua b/ext/Server/BotChatter/Packs/NZ.lua new file mode 100644 index 00000000..6705f3d5 --- /dev/null +++ b/ext/Server/BotChatter/Packs/NZ.lua @@ -0,0 +1,61 @@ +-- ext/Server/BotChatter/Packs/NZ.lua +-- Code by: JMDigital (https://github.com/JenkinsTR) +-- NZ flavour: subtle, clean, ASCII. Light use of 'sweet as', 'choice', 'chur'. No memes. +local NZ = {} + +NZ.Lines = { + Kill = { + "sweet as.","too easy.","choice pick.","clean work.","good as.","nice peel.", + "cheers for the peek.","sorted.","no worries.","that'll do.","mint shot." + }, + Death = { + "yep, fair.","he got me.","good one.","yeah nah.","earned that.","stiff as.","timing got me.","my bad there." + }, + Spawn = { + "i'm in.","right behind you.","we're on.","ok, send it.","warming up.","keen as.","regrouping.","moving now." + }, + Headshot = { "clean as.","mint flick.","one tap.","right between the eyes.","crispy.","head gone." }, + Revenge = { "even as.","that's for before.","we're square.","sorted now." }, + Roadkill = { "mind the ute.","watch the bumper.","that was rough.","road's closed.","whoops." }, + RoundStartGlobal = { "gl hf","play tidy.","good as gold.","keep it clean." }, + RoundEndGlobal = { "gg","nice work.","that was decent.","cheers team." }, + + VehicleKill = { + "armor cracked.","bird down.","vehicle disabled.","tank gone.","pilot out.","ride's done.", + "tracks busted.","driver bailed.","aa down.","rotor stopped.","engine out.","free scrap." + }, + Multi2 = { "double.","two down.","tempo up.","chain started.","easy two.","momentum.","keep feeding." }, + Multi3 = { "triple.","three piece.","they're falling apart.","on a tear.","lining up.","ok, who's next?" }, + Multi4 = { "multi on.","they can't stop me.","stack wiped.","send more, please.","getting rude now :)" }, + Streak = { "on a run.","untouchable.","they can't trade me.","farm mode.","heat check.","everything's clicking." }, + Longshot = { "long one.","range diff.","i own that sightline.","greetings from downtown.","too comfy at range." }, + VehEnter = { "taking a ride.","mounting up.","i'm in.","driver ready.","gunning.","vroom." }, + VehExit = { "bailing.","hopping out.","on foot.","ditch the ride.","ground game now.","fresh air." }, + FirstBlood = { "first blood.","opening pick.","we start strong.","tempo set.","good start." }, + + -- Named variants + KillNamed = { + "gg {enemy}.","sit down, {enemy}.","trade denied, {enemy}.","peeked wrong, {enemy}.", + "outplayed, {enemy}.","see you at respawn, {enemy}.","angle was mine, {enemy}." + }, + HeadshotNamed = { + "one tap, {enemy}.","keep your head down, {enemy}.","peek punished, {enemy}.","clean head, {enemy}.","crispy on you, {enemy}." + }, + LongshotNamed = { "long one, {enemy}.","range diff, {enemy}.","eagle eye on you, {enemy}.","hello from far away, {enemy}." }, + RevengeNamed = { "that's for earlier, {enemy}.","we're even, {enemy}.","payback delivered, {enemy}.","circle closed, {enemy}." }, + VehicleKillNamed = { "bye armor, {enemy}.","driver out, {enemy}.","bird down, {enemy}.","nice ride, {enemy}... was." }, + RoadkillNamed = { "mind the wheels, {enemy}.","hood ornament unlocked, {enemy}.","green light, {enemy}.","street pizza, {enemy}." }, + DeathNamed = { "nice shot, {enemy}.","ok you got me, {enemy}.","pre-aimed me, {enemy}.","good timing, {enemy}.","fair as, {enemy}." }, + + -- Weapon-specific + KnifeKill = { "shh.","silent op.","that was personal.","slice and dice.","backstab meta." }, + GrenadeKill = { "cook perfect.","frag lands.","nade out.","catch.","boom timing.","bank shot." }, + ShotgunKill = { "close range diff.","12g says hi.","open the door.","point blank.","boomstick meta." }, + PistolKill = { "sidearm gaming.","pistol whipped.","secondary supremacy.","tap tap.","backup did it." }, + SniperKill = { "scope sings.","steady hands.","click at range.","lane owned.","glass cannon online." }, +} + +NZ.Tweaks = { casing = "lower", distort = { emoticonChance = 0.12 } } +NZ.PersonalityBias = { Chill = 1.2, Tactical = 1.0, Cocky = 0.9, Sassy = 0.95 } + +return { id = "NZ", Lines = NZ.Lines, Tweaks = NZ.Tweaks, PersonalityBias = NZ.PersonalityBias } diff --git a/ext/Server/BotChatter/Packs/UK.lua b/ext/Server/BotChatter/Packs/UK.lua new file mode 100644 index 00000000..5b1949e7 --- /dev/null +++ b/ext/Server/BotChatter/Packs/UK.lua @@ -0,0 +1,63 @@ +-- ext/Server/BotChatter/Packs/UK.lua +-- Code by: JMDigital (https://github.com/JenkinsTR) +-- UK flavour: understated, dry, tidy. ASCII only. +local UK = {} + +UK.Lines = { + Kill = { + "sorted.","lovely.","nice one.","job done.","cheeky pick.", + "clean work.","that'll do.","on to the next.","had him on ropes.","textbook." + }, + Death = { + "fair play.","well taken.","you got me.","he earned that.","right, lesson learned.", + "timing got me.","good angle from them.","deserved, that.","bit of a shambles from me." + }, + Spawn = { + "back in.","moving.","let's roll.","forming up.","on you.","regrouping.", + "focus on.","alright then.","keep it neat.","eyes open." + }, + Headshot = { "clean between the eyes.","crisp.","keep your head down.","neat shot.","one tap." }, + Revenge = { "we're square.","consider it even.","that settles it.","paid back." }, + Roadkill = { "mind the bumper.","road's shut.","apologies to the bonnet.","street's closed." }, + RoundStartGlobal = { "gl hf","keep it tidy.","play sensible.","nice and clean." }, + RoundEndGlobal = { "gg","well played.","nice one.","cheers." }, + + VehicleKill = { + "armor cracked.","bird down.","vehicle disabled.","tank gone.","pilot out.", + "tracks busted.","driver bailed.","aa down.","rotor stopped.","engine out." + }, + Multi2 = { "double.","two down.","tempo up.","chain started.","easy two.","momentum." }, + Multi3 = { "triple.","three piece.","they're falling apart.","on a tear.","lining up." }, + Multi4 = { "multi on.","they can't stop me.","stack wiped.","send more, please." }, + Streak = { "on a run.","untouchable.","they can't trade me.","farm mode.","heat check." }, + Longshot = { "long one.","owning that lane.","distance handled.","postcard range.","steady at range." }, + VehEnter = { "climbing in.","mounting up.","driver ready.","gunning.","moving." }, + VehExit = { "bailing.","hopping out.","on foot.","ground game.","fresh air." }, + FirstBlood = { "first blood.","opening pick.","off to a tidy start.","tempo set." }, + + -- Named variants + KillNamed = { + "gg {enemy}.","sit down, {enemy}.","trade denied, {enemy}.","peeked wrong, {enemy}.", + "outplayed, {enemy}.","see you at respawn, {enemy}." + }, + HeadshotNamed = { + "one tap, {enemy}.","keep your head down, {enemy}.","peek punished, {enemy}.","clean head, {enemy}." + }, + LongshotNamed = { "long one, {enemy}.","range diff, {enemy}.","eyes on you from afar, {enemy}." }, + RevengeNamed = { "we're even, {enemy}.","that settles it, {enemy}.","paid back, {enemy}." }, + VehicleKillNamed = { "bye armor, {enemy}.","driver out, {enemy}.","bird down, {enemy}." }, + RoadkillNamed = { "mind the wheels, {enemy}.","street pizza, {enemy}.","green light, {enemy}." }, + DeathNamed = { "nice shot, {enemy}.","you got me, {enemy}.","pre-aimed me, {enemy}.","timing favoured you, {enemy}." }, + + -- Weapon-specific + KnifeKill = { "quiet now.","silent work.","personal.","slice and tidy.","backstab done." }, + GrenadeKill = { "cook spot on.","frag lands.","nade out.","catch.","good bank." }, + ShotgunKill = { "close range settled.","12g says hello.","door's open.","point blank." }, + PistolKill = { "sidearm did it.","pistol work.","secondary wins.","tap tap." }, + SniperKill = { "scope sings.","steady hands.","lane owned.","click at range." }, +} + +UK.Tweaks = { casing = "lower", distort = { emoticonChance = 0.08 } } +UK.PersonalityBias = { Chill = 1.0, Tactical = 1.1, Cocky = 0.95, Sassy = 1.0 } + +return { id = "UK", Lines = UK.Lines, Tweaks = UK.Tweaks, PersonalityBias = UK.PersonalityBias } diff --git a/ext/Server/BotChatter/Personalities.lua b/ext/Server/BotChatter/Personalities.lua new file mode 100644 index 00000000..6bdca82e --- /dev/null +++ b/ext/Server/BotChatter/Personalities.lua @@ -0,0 +1,56 @@ +-- ext/Server/BotChatter/Personalities.lua +-- Code by: JMDigital (https://github.com/JenkinsTR) +return { + Chill = { + Kill = { "nice.", "clean one.", "ok then.", "steady aim.", "smooth.", "we chillin'." }, + Death = { "all good.", "meh.", "ok.", "respawn life.", "no worries.", "we reset." }, + Spawn = { "back in.", "alright.", "i'm here.", "ready again.", "slow push.", "stay cool." }, + Headshot = { "nice tap.", "one clean.", "quick flick.", "lined up.", "good click." }, + VehicleKill = { "vehicle down.", "bye wheels.", "ride over.", "engine sad." }, + Revenge = { "even.", "balance.", "we're square.", "order restored." }, + Roadkill = { "watch your toes.", "roll through.", "whoops." }, + Longshot = { "long poke.", "calm hands.", "that was far." }, + VehEnter = { "ride time.", "let's move.", "i'll drive.", "gunning." }, + VehExit = { "on foot.", "bail.", "grounded." }, + FirstBlood = { "nice open.", "ok we're live.", "tempo on." } + }, + Cocky = { + Kill = { "ez.", "sit down.", "outplayed.", "deleted.", "aim diff." }, + Death = { "lucky.", "fine.", "rematch.", "ok...", "warm up round." }, + Spawn = { "back for more.", "reload the feed.", "round 2.", "still here." }, + Headshot = { "head clean.", "crispy.", "one tap.", "stay crouched." }, + VehicleKill = { "nice ride, mine now.", "pop the tank.", "scrap metal." }, + Revenge = { "payback.", "receipt printed.", "we're even now." }, + Roadkill = { "drive-by certified.", "street sweeper.", "fresh tread." }, + Longshot = { "count it.", "distance diff.", "laser pointer." }, + VehEnter = { "chauffeur time.", "shotgun, thanks.", "vroom." }, + VehExit = { "walk it off.", "legs online.", "jump out play." }, + FirstBlood = { "first try.", "opening act.", "who's next." } + }, + Tactical = { + Kill = { "clear.", "target down.", "advance.", "angle secured." }, + Death = { "down.", "taking fire.", "fallback.", "regroup." }, + Spawn = { "in play.", "support inbound.", "on your six.", "cover me." }, + Headshot = { "head neutral.", "clean angle.", "lane controlled." }, + VehicleKill = { "armor neutral.", "vehicle offline.", "road clear." }, + Revenge = { "trade secured.", "threat answered.", "balance achieved." }, + Roadkill = { "overrun.", "path cleared.", "bypass complete." }, + Longshot = { "distance control.", "sightline secured.", "pressure at distance." }, + VehEnter = { "driver in.", "gunner ready.", "mobile now." }, + VehExit = { "dismount.", "switching posture.", "ground move." }, + FirstBlood = { "opening frag.", "lead established.", "initiative taken." } + }, + Sassy = { + Kill = { "boop.", "thanks for playing.", "caught you.", "talk to the killcam :)" }, + Death = { "rude.", "that's illegal.", "ok hacker.", "fine, take your clip." }, + Spawn = { "and i'm back.", "miss me?", "part 2.", "rebooted." }, + Headshot = { "yikes.", "oopsie head.", "snap!", "hat came off." }, + VehicleKill = { "uber cancelled.", "helicrash!", "repo complete." }, + Revenge = { "remember me?", "kiss the ring.", "karma speedrun." }, + Roadkill = { "street pizza.", "gta moment.", "mind the crosswalk." }, + Longshot = { "rent's due from range.", "zoom zoom click.", "sniper cosplay." }, + VehEnter = { "road trip.", "shotgun!", "vroom vroom." }, + VehExit = { "see ya car.", "walking sim.", "touching grass." }, + FirstBlood = { "main character energy.", "opening act slaps.", "we gaming." } + } +} diff --git a/ext/Server/BotChatter/Util.lua b/ext/Server/BotChatter/Util.lua new file mode 100644 index 00000000..0143f07f --- /dev/null +++ b/ext/Server/BotChatter/Util.lua @@ -0,0 +1,25 @@ +-- ext/Server/BotChatter/Util.lua +-- Code by: JMDigital (https://github.com/JenkinsTR) +local U = {} + +function U.strip_tags(name) + if not name then return "" end + -- Remove leading bracketed tags: [AU], (AU), {AU} + return (name:gsub("^%b[]", ""):gsub("^%b()", ""):gsub("^%b{}", "")):gsub("^%s+", "") +end + +function U.simple_hash(s, seed) + local h = seed or 1337 + for i = 1, #s do h = (h * 33 + s:byte(i)) % 2147483647 end + return h +end + +function U.merge_arrays(a, b, c) + local t = {} + if a then for i=1,#a do t[#t+1] = a[i] end end + if b then for i=1,#b do t[#t+1] = b[i] end end + if c then for i=1,#c do t[#t+1] = c[i] end end + return t +end + +return U diff --git a/ext/Server/BotUpdate.lua b/ext/Server/BotUpdate.lua new file mode 100644 index 00000000..d0ef91ee --- /dev/null +++ b/ext/Server/BotUpdate.lua @@ -0,0 +1,311 @@ +---@type NodeCollection +local m_NodeCollection = require('NodeCollection') +---@type Vehicles +local m_Vehicles = require('Vehicles') +---@type AirTargets +local m_AirTargets = require('AirTargets') +---@type Logger +local m_Logger = Logger('Bot', Debug.Server.BOT) + +local m_BotAiming = require('Bot/BotAiming') +local m_BotAttacking = require('Bot/BotAttacking') +local m_BotMovement = require('Bot/BotMovement') +local m_BotWeaponHandling = require('Bot/BotWeaponHandling') +local m_VehicleAiming = require('Bot/VehicleAiming') +local m_VehicleAttacking = require('Bot/VehicleAttacking') +local m_VehicleMovement = require('Bot/VehicleMovement') +local m_VehicleWeaponHandling = require('Bot/VehicleWeaponHandling') + + +-- Update frame (every Cycle). +-- Update very fast (0.05) ? Needed? Aiming? +-- Update fast (0.1) ? Movement, Reactions. +-- (Update medium? Maybe some things in between). +-- Update slow (1.0) ? Reload, Deploy, (Obstacle-Handling). + +---@param p_DeltaTime number +function Bot:OnUpdatePassPostFrame(p_DeltaTime) + local s_Soldier = self.m_Player.soldier + + -- Bot not alive, check for respawn + if not s_Soldier then + self._UpdateTimer = self._UpdateTimer + p_DeltaTime -- Reusage of updateTimer. + + if self._UpdateTimer > Registry.BOT.BOT_UPDATE_CYCLE then + self:_UpdateRespawn(Registry.BOT.BOT_UPDATE_CYCLE) + self._UpdateTimer = 0.0 + end + + return + end + + s_Soldier:SingleStepEntry(self.m_Player.controlledEntryId) + + -- Bot cannot move yet. + if not Globals.IsInputAllowed or self._SpawnProtectionTimer > 0.0 then + -- Alive, but no inputs allowed yet → look around. + self._UpdateTimer = self._UpdateTimer + p_DeltaTime -- Reusage of updateTimer. + + if self._UpdateTimer > Registry.BOT.BOT_UPDATE_CYCLE then + if self._SpawnProtectionTimer > 0.0 then + self._SpawnProtectionTimer = self._SpawnProtectionTimer - Registry.BOT.BOT_UPDATE_CYCLE + else + self._SpawnProtectionTimer = 0.0 + end + + m_BotMovement:UpdateYaw(self) + m_BotMovement:LookAround(self, Registry.BOT.BOT_UPDATE_CYCLE) + self:_UpdateInputs() + self._UpdateTimer = 0.0 + end + return + end + + -- Update timer. + self._UpdateFastTimer = self._UpdateFastTimer + p_DeltaTime + + if self._UpdateFastTimer >= Registry.BOT.BOT_FAST_UPDATE_CYCLE then + -- Fast timer function will call the slow timer functions + self:FastTimerUpdate(p_DeltaTime) + self._UpdateFastTimer = 0.0 + end + + -- NOTE: Very fast code. + -- Update yaw of soldier every tick. + if not self.m_InVehicle then + m_BotMovement:UpdateYaw(self) + end +end + +---comment +---@param p_DeltaTime number +function Bot:FastTimerUpdate(p_DeltaTime) + -- Increment slow timer. + self._UpdateTimer = self._UpdateTimer + self._UpdateFastTimer + + -- Detect modes. + self:_SetActiveVars() + + -- Old movement-modes -- remove one day? + if self:IsStaticMovement() then + m_BotMovement:UpdateStaticMovement(self) + self:_UpdateInputs() + m_BotMovement:UpdateYaw(self) + self._UpdateFastTimer = 0.0 + return + end + + -- Timeout after revive. Do nothing. + if self._ActiveDelay > 0.0 then + self._ActiveDelay = self._ActiveDelay - p_DeltaTime + if self._ActiveDelay <= 0.0 and self.m_Player.soldier ~= nil then + -- accept revive + self:_SetInput(EntryInputActionEnum.EIACycleRadioChannel, 1) + self:_UpdateInputs() + self.m_Player.soldier:SetPose(CharacterPoseType.CharacterPoseType_Crouch, true, true) + self._SpawnDelayTimer = 0.0 --reset spawn-delay on revive + end + return + end + + ------------------ CODE OF BEHAVIOUR STARTS HERE --------------------- + local s_IsAttacking = self._ShootPlayer ~= nil -- Can be either attacking or reviving or enter of a vehicle with a player. + + -- Bot is a Passenger of a boat, for example. + if self.m_OnVehicle then + self:OnVehicleFastTimerUpdate(s_IsAttacking) + return + end + + -- Bot is in a vehicle + if self.m_InVehicle then + self:InVehicleFastTimerUpdate(s_IsAttacking) + return + end + + -- Sync slow code with fast code. Therefore, execute the slow code first. + if self._UpdateTimer >= Registry.BOT.BOT_UPDATE_CYCLE then + self:SoldierSlowTimerUpdate(s_IsAttacking) + end + + -- Fast code. + if s_IsAttacking then + m_BotAiming:UpdateAiming(self) + else + m_BotMovement:UpdateTargetMovement(self) + end +end + +---comment +---@param p_IsAttacking boolean +function Bot:SoldierSlowTimerUpdate(p_IsAttacking) + -- Common part. + m_BotWeaponHandling:UpdateWeaponSelection(self) + + -- Differ attacking. + if p_IsAttacking then + m_BotAttacking:UpdateAttacking(self) + if self._ActiveAction == BotActionFlags.ReviveActive or + self._ActiveAction == BotActionFlags.EnterVehicleActive or + self._ActiveAction == BotActionFlags.RepairActive or + self._ActiveAction == BotActionFlags.C4Active then + m_BotMovement:UpdateMovementSprintToTarget(self) + else + m_BotMovement:UpdateShootMovement(self) + end + else + m_BotWeaponHandling:UpdateDeployAndReload(self, true) + m_BotMovement:UpdateNormalMovement(self) + if self.m_Player.soldier == nil then + return + end + end + + -- Common things. + m_BotMovement:UpdateSpeedOfMovement(self) + self:_UpdateInputs() + + self._UpdateTimer = 0.0 +end + +---comment +---@param p_IsAttacking boolean +function Bot:OnVehicleFastTimerUpdate(p_IsAttacking) + -- Sync slow code with fast code. Therefore, execute the slow code first. + if self._UpdateTimer >= Registry.BOT.BOT_UPDATE_CYCLE then + -- Common part. + m_BotWeaponHandling:UpdateWeaponSelection(self) + + -- Differ attacking. + if p_IsAttacking then + m_BotAttacking:UpdateAttacking(self) + else + m_BotWeaponHandling:UpdateDeployAndReload(self, false) + end + + self:_UpdateInputs() + self:_CheckForVehicleActions(self._UpdateTimer, p_IsAttacking) + + -- Only exit at this point and abort afterwards. + if self:_DoExitVehicle() then + return + end + + self._UpdateTimer = 0.0 + end + + -- Fast code. + if p_IsAttacking then + m_BotAiming:UpdateAiming(self) + else + self:_UpdateLookAroundPassenger(Registry.BOT.BOT_FAST_UPDATE_CYCLE) + end +end + +---comment +---@param p_IsAttacking boolean +function Bot:InVehicleFastTimerUpdate(p_IsAttacking) + -- Stationary AA needs separate handling. + if m_Vehicles:IsVehicleType(self.m_ActiveVehicle, VehicleTypes.StationaryAA) then + self:_UpdateStationaryAAVehicle(p_IsAttacking) + + if self._UpdateTimer >= Registry.BOT.BOT_UPDATE_CYCLE then + -- Common part. + m_VehicleWeaponHandling:UpdateWeaponSelectionVehicle(self) + + -- Differ attacking. + if p_IsAttacking then + m_VehicleAttacking:UpdateAttackStationaryAAVehicle(self) + end + + self:_UpdateInputs() + self._UpdateTimer = 0.0 + + self:_DoExitVehicle() + end + + return + end + + if m_Vehicles:IsVehicleType(self.m_ActiveVehicle, VehicleTypes.Plane) then + -- assign new target after some time + if self._DeployTimer > (Config.BotVehicleFireModeDuration - 0.5) and self._VehicleTakeoffTimer <= 0.0 then + local s_Target = m_AirTargets:GetTarget(self.m_Player, Registry.VEHICLES.MAX_ATTACK_DISTANCE_JET) + + if s_Target ~= nil then + self._ShootPlayerName = s_Target.name + self._ShootPlayer = PlayerManager:GetPlayerByName(self._ShootPlayerName) + self._ShootPlayerVehicleType = m_Vehicles:FindOutVehicleType(self._ShootPlayer) + self._ShootModeTimer = Config.BotVehicleFireModeDuration + else + self:AbortAttack() + end + + self._DeployTimer = 0.0 + else + self._DeployTimer = self._DeployTimer + Registry.BOT.BOT_FAST_UPDATE_CYCLE + end + end + + local s_IsStationaryLauncher = m_Vehicles:IsVehicleType(self.m_ActiveVehicle, VehicleTypes.StationaryLauncher) or m_Vehicles:IsVehicleType(self.m_ActiveVehicle, VehicleTypes.StationaryAA) + + -- Sync slow code with fast code. Therefore, execute the slow code first. + if self._UpdateTimer >= Registry.BOT.BOT_UPDATE_CYCLE then + -- Common part. + m_VehicleWeaponHandling:UpdateWeaponSelectionVehicle(self) + + -- Differ attacking. + if p_IsAttacking then + m_VehicleAttacking:UpdateAttackingVehicle(self) + if Config.VehicleMoveWhileShooting and m_Vehicles:IsNotVehicleTerrain(self.m_ActiveVehicle, VehicleTerrains.Air) then + if self.m_Player.controlledEntryId == 0 and not s_IsStationaryLauncher then -- Only if driver. + m_VehicleMovement:UpdateNormalMovementVehicle(self) + else + m_VehicleMovement:UpdateShootMovementVehicle(self) + end + else + m_VehicleMovement:UpdateShootMovementVehicle(self) + end + else + m_VehicleWeaponHandling:UpdateReloadVehicle(self) + if self.m_Player.controlledEntryId == 0 and not s_IsStationaryLauncher then -- Only if driver. + m_VehicleMovement:UpdateNormalMovementVehicle(self) + end + end + + -- Common things. + m_VehicleMovement:UpdateSpeedOfMovementVehicle(self, p_IsAttacking) + self:_UpdateInputs() + self:_CheckForVehicleActions(self._UpdateTimer, p_IsAttacking) + + -- Only exit at this point and abort afterwards. + if self:_DoExitVehicle() then + return + end + + self._UpdateTimer = 0.0 + end + + -- Fast code. + if p_IsAttacking then + if m_Vehicles:IsAirVehicle(self.m_ActiveVehicle) then + m_VehicleAiming:UpdateAimingVehicle(self, true) + else + if Config.VehicleMoveWhileShooting and m_Vehicles:IsNotVehicleTerrain(self.m_ActiveVehicle, VehicleTerrains.Air) then + if self.m_Player.controlledEntryId == 0 and not s_IsStationaryLauncher then -- Only if driver. + -- also update movement + m_VehicleMovement:UpdateTargetMovementVehicle(self) + end + end + m_VehicleAiming:UpdateAimingVehicle(self, false) + end + else + if self.m_Player.controlledEntryId == 0 and not s_IsStationaryLauncher then -- Only if driver. + m_VehicleMovement:UpdateTargetMovementVehicle(self) + else + m_VehicleMovement:UpdateVehicleLookAround(self, self._UpdateFastTimer) + end + end + + m_VehicleMovement:UpdateYawVehicle(self, p_IsAttacking, s_IsStationaryLauncher) +end diff --git a/ext/Server/__init__.lua b/ext/Server/__init__.lua index 94873f69..6667e91a 100644 --- a/ext/Server/__init__.lua +++ b/ext/Server/__init__.lua @@ -38,6 +38,9 @@ require('UIPathMenu') require('Model/Globals') require('Constants/Permissions') +-- load the chat hooks +require('BotChatter') + ---@type Logger local m_Logger = Logger("FunBotServer", Debug.Server.INFO) diff --git a/ext/Shared/BotChatterConfig.lua b/ext/Shared/BotChatterConfig.lua new file mode 100644 index 00000000..3b08fb92 --- /dev/null +++ b/ext/Shared/BotChatterConfig.lua @@ -0,0 +1,50 @@ +-- ext/Shared/BotChatterConfig.lua +-- Code by: JMDigital (https://github.com/JenkinsTR) +return { + -- Which pack to use if no per-bot tag is matched. + defaultPack = "Default", + + -- Let bot name tags select a regional pack, e.g. "[AU]Gaz". + allowPerBotPackByTag = true, + + -- Keys are case-insensitive prefixes to match at start of name. + -- Values are pack IDs (folder/file names in BotChatter/Packs). + tagToPack = { + ["[AU]"] = "AU", + ["(AU)"] = "AU", + ["{AU}"] = "AU", + ["[NZ]"] = "NZ", + ["(NZ)"] = "NZ", + ["{NZ}"] = "NZ", + ["[UK]"] = "UK", + ["(UK)"] = "UK", + ["{UK}"] = "UK", + ["[CA]"] = "CA", + ["(CA)"] = "CA", + ["{CA}"] = "CA", + ["[US]"] = "Default", -- US bots fall back to Default chatter + }, + + -- Personality assignment: + -- "seeded" (stable per-bot), "single" (force one for all), "randomEachEvent" + personalityMode = "seeded", + forcedPersonality = "Tactical", + + -- Deterministic seed for personalities, etc. + seed = 1337, + + -- Global distortion knobs (packs can override bits of this) + distort = { + enabled = true, + emoticonChance = 0.12, -- append a text emoticon sometimes + elongateChance = 0.06, -- "niice" + uppercaseBurstChance = 0.05, + maxElongate = 3, + }, + + -- Safety: limit how many lines per bot per time window (anti-spam) + rateLimit = { + windowSec = 8.0, + maxPerWindow = 2 + } +} diff --git a/ext/Shared/Constants/BotNames.lua b/ext/Shared/Constants/BotNames.lua index 00d73cc1..506b60f0 100644 --- a/ext/Shared/Constants/BotNames.lua +++ b/ext/Shared/Constants/BotNames.lua @@ -1,564 +1,17 @@ ---@class BotNames -BotNames = { - -- Normal Names. - 'Liam', - 'Noah', - 'Oliver', - 'William', - 'Elijah', - 'James', - 'Benjamin', - 'Lucas', - 'Mason', - 'Ethan', - 'Alexander', - 'Henry', - 'Jacob', - 'Michael', - 'Daniel', - 'Logan', - 'Jackson', - 'Sebastian', - 'Jack', - 'Aiden', - 'Owen', - 'Samuel', - 'Matthew', - 'Joseph', - 'Levi', - 'Mateo', - 'John', - 'Wyatt', - 'Carter', - 'Julian', - 'Luke', - 'Grayson', - 'Isaac', - 'Jayden', - 'Theodore', - 'Gabriel', - 'Anthony', - 'Dylan', - 'Leo', - 'Lincoln', - 'Jaxon', - 'Asher', - 'Josiah', - 'Andrew', - 'Thomas', - 'Joshua', - 'Ezra', - 'Hudson', - 'Charles', - 'Caleb', - 'Isaiah', - 'Ryan', - 'Nathan', - 'Adrian', - 'Christian', - 'Maverick', - 'Colton', - 'Elias', - 'Aaron', - 'Eli', - 'Landon', - 'Jonathan', - 'Nolan', - 'Hunter', - 'Cameron', - 'Connor', - 'Santiago', - 'Jeremiah', - 'Ezekiel', - 'Angel', - 'Roman', - 'Easton', - 'Miles', - 'Robert', - 'Jameson', - 'Nicholas', - 'Greyson', - 'Cooper', - 'Ian', - 'Carson', - 'Axel', - 'Jaxson', - 'Dominic', - 'Leonardo', - 'Luca', - 'Austin', - 'Jordan', - 'Adam', - 'Xavier', - 'Jose', - 'Jace', - 'Everett', - 'Declan', - 'Evan', - 'Kayden', - 'Parker', - 'Wesley', - 'Kai', - 'Lesley', - 'Sparrow', - 'Rudy', - 'Lars', - 'Marlon', - 'Tim', - 'Tom', - 'Bob', - 'Lianne', - 'Nelson', - 'Herald', - 'Johan', - 'Jason', - 'Sean', - 'Trevor', - 'Steve', - 'Jeff', - 'Edward', - 'Aren', - 'Zachary', - 'Pete', - 'Diana', - 'Riemmelth', - 'Craig', - 'Lew', - 'Christina', - 'George', - 'Horst', - 'Romeo', - 'Erwin', - 'Robbe', - 'Joker', - 'Rube', - 'Leon', - 'Zaviar', - -- Devs. - 'Bizzi', - 'FlashHit', - 'Bree', - 'Arnold', - 'ThyKingdomCome', - 'Major', - 'Joe', - 'Firjen', - 'Sjoerd', - 'Kiwi', - 'NoFaTe', - 'Paul', - 'Beschützer', - -- Contributors. - 'BOB', - 'MeisterPeitsche', - 'DuTcHrEaGaN', - 'SmartShots', - 'RekkieSA', - 'MrDonPotato', - 'KrazyIvan', - 'GaryTheNoTrashCougar', - 'Augusta', - 'Rolling Waves', - -- Supporters. - 'jacky890818', - 'Cosmin', - 'qnr3343', - 'akebono047', - 'EricH', - -- Other names - 'LunyblacAseity', - 'C4EvC4Elius', - 'ivan0394', - 'tagener-noisuf', - 'Aseityja753', - 'WordplayAmelus', - 'Ameluslan93', - 'EvmonPapren', - 'Jay_legFusee', - 'Fuseered2', - 'Paprenjak270101', - 'Crayos2Ruber', - 'BevusGabion', - 'AstawNiff', - 'Niffper1289', - 'Junan6Mazut', - 'Mazutwerty89983nr2', - 'CharpleZincic', - 'Lollapalooza', - 'MrcheeksUncate', - 'Uncatemon369', - 'KnittedFuligo', - 'Fuligostervdb', - 'TriutLunacy', - 'Lunacytcb0987', - 'BagladyLablab', - 'Lablabpon97', - 'CanotMullet', - 'Mulletytedy', - 'GarnierRubigo', - 'Rubigova252', - 'KormiXyston', - 'MillstonEbb', - 'Ebbmily3452', - 'Ruborooo123', - 'FrugieNoyade', - 'Noyadetes9', - 'SchmidtdoggTabard', - 'Tabardta38x', - 'BuchnerAgnosy', - 'ExplodLar', - 'Larle1511', - 'AnnieKitsch', - 'Kitschsen56', - 'IzyadxIberis', - 'MazolCenoby', - 'Cenobyrier13', - 'ThecudBummel', - 'Bummelro_l', - 'Mr_pookPteric', - 'ProajB0lide', - 'Bolidewe789', - 'AdaiKava', - 'Kavapburnz', - 'Jossis0Lumen', - 'Lumenko125', - 'HarunLydian', - 'AcrolEclat', - 'EllaunTazza', - 'Tazzaka2003', - 'BuffyblonKylin', - 'Kylinmo773', - 'DibujFleam', - 'Fleamderb10', - 'IfonModish', - 'Modishhost7518', - 'LptvClem', - 'Clemlue7x', - 'FlexilSabin', - 'Sabinfoot12', - 'UnzaaTeen', - 'Teenman93', - 'McdoubObtest', - 'Obtestruixr', - 'Mikie9Humect', - 'Humecttout40', - 'ZaredEchard', - 'Echardle_dk', - 'KademCotta', - 'Cottatley650', - 'KilemQiviut', - 'Qiviutzzxzxc', - 'Glove_Fumage', - 'Fumagevel800', - 'Layon3Olid', - 'Olidkeks', - 'Foudroyant', - 'Stasis2Credo', - 'Credoder_166', - 'Klendusic', - --[['LordchaosVirtu', - 'Virtuheep', - 'Sphecoid', - 'Arshan9Maven', - 'Mavenrits21', - 'W1ddDank', - 'Dank3achlp', - 'The_beasToats', - 'Toatsverbr', - 'Karis3Kurgan', - 'Kurganro_x', - 'Blaze12Trover', - 'Troverle789', - 'GixanMormal', - 'Mormalni89', - 'MpoodNorsel', - 'Norselshock21', - 'CellerEtude', - 'Etudeqon74', - 'EsgarWithy', - 'Withyveach', - 'JuliaPlew', - 'Plewko1998', - 'SexyhamZymite', - 'Zymiterae7910819', - 'BropotAlow', - 'Alowtors619', - 'ThefurLimn', - 'Limnve0880', - 'CracklejTerret', - 'Terretverzz', - 'FauxPas', - 'XminebMehari', - 'Meharimerr', - 'Tjsutan2Vacky', - 'Vackyper5369', - 'LigneouSkua', - 'Skua_los', - 'MisadToison', - 'Toisoncven', - 'GarberArdass', - 'Ardassmmer', - 'Archer7Porge', - 'Porgeton1573', - 'LegalLilt', - 'Liltbis28', - 'PuppeedTercet', - 'Tercetdomdr', - 'CrazyphatPiffle', - 'Pifflelias23', - 'PmanscoutPieta', - 'Pietamicr', - 'AdvocMensal', - 'Mensalyre0', - 'PesherDammar', - 'Dammargeryk', - 'OctantNivial', - 'Niviallen3586', - 'AmendSiege', - 'Siegebarmy99', - 'ChiyuCrista', - 'Cristatcp7988', - 'TiconTund', - 'AlfiewQuotum', - 'Quotumchak', - 'TharealSimony', - 'Simonybrad', - 'McfabistPlacet', - 'Placetcest0ry', - 'VortexxFlench', - 'Flenchboss_921', - 'FazomMaya', - 'Mayanut5205', - 'Phthartic', - 'JaphetAcracy', - 'Acracyduck13', - 'SubcomBuffe', - 'Buffeyes3041', - 'WorgerIsovol', - 'Isovolrias01', - 'Djdave4Vacky', - 'Vackyja45', - 'MesocYote', - 'Yotegor141201', - 'Pinti_1Morgue', - 'Morguelden23', - 'MintboltPi', - 'Pirom101', - 'AllygrabGuidon', - 'Guidonwarf12', - 'NanidrGayal', - 'Gayal2kill47', - 'FullbodDumose', - 'Dumoseka6658', - 'BiggunsCadge', - 'Cadgesel15', - 'SigilKosher', - 'Kosherker965', - 'PostulGeoid', - 'Geoidbe194', - 'Accoucheuse', - 'GecutMimosa', - 'Mimosaves228', - 'DinitSachet', - 'Sachetjaak', - 'FirstmatMnesic', - 'Mnesicwarf132', - 'CobayDiable', - 'Diablemad0002468', - 'LarobNoyade', - 'Noyadegox72', - 'Tang_tifSamara', - 'Samaraperz3', - 'AjtakLeech', - 'Leechreeww', - 'FerromPlenum', - 'Plenumnoahs8282', - 'ReikKinkle', - 'Kinkle3wiz0', - 'MaggiorZona', - 'Zonavert246', - 'ProemAgouti', - 'Agouticial_hd', - 'LlleonlUmbel', - 'Umbeljem99', - 'CalvaMbira', - 'Mbirabelbc123', - 'HomeoSorrel', - 'Sorreldel567', - 'Xystarch', - 'ParthenFlacon', - 'Flacongalz432', - 'Triptych', - 'SalipAlgor', - 'Algorperz911', - 'Flivver', - 'LordramIrpe', - 'RikuhKore', - 'Korekristyn', - 'CandytuftOread', - 'Oreadpkids10', - 'NumafSallet', - 'Salletmon1236', - 'PajokDolmen', - 'Dolmendeath765', - 'Platysma', - 'HistryboyFretum', - 'Fretumder2436', - 'SamhitRoup', - 'BillatAraba', - 'Arabayingd25', - 'ShipcraftHiant', - 'Hiantrain666', - 'Goku11Creant', - 'FluddcarXyston', - 'Xystondcert', - 'SextantStythe', - 'Stythelithx', - 'TitoisBifid', - 'Bifidcat27', - 'LammondEre', - 'Erelilly39', - 'OggefMuslin', - 'Muslinlaus10', - 'Diphyodont', - 'JedrekDiable', - 'Diablercs_1', - 'EcnerBarn', - 'Barnman38' ]] -} +-- Code by: JMDigital (https://github.com/JenkinsTR) +-- This file now loads the NameProvider module instead of hard-coding names here. ----@class BotNames -USMarinesNames = { - 'Pvt. Jackson', - 'Cpl. Ramirez', - 'Sgt. Thompson', - 'Lt. Krigs', - 'Capt. Anderson', - 'Maj. Carter', - 'Col. Reynolds', - 'Pvt. Walker', - 'Cpl. Nguyen', - 'Sgt. Patel', - 'Lt. Bennett', - 'Capt. Fisher', - 'Maj. Brooks', - 'Col. Hayes', - 'Pvt. Green', - 'Cpl. Turner', - 'Sgt. Morgan', - 'Lt. Sanders', - 'Capt. Wells', - 'Maj. Griffin', - 'Col. Price', - 'Pvt. Mitchell', - 'Cpl. Lee', - 'Sgt. Edwards', - 'Lt. Clark', - 'Capt. Rivera', - 'Maj. Foster', - 'Col. Cooper', - 'Pvt. Lewis', - 'Cpl. King', - 'Sgt. Roberts', - 'Lt. Morales', - 'Capt. Shaw', - 'Maj. Bennett', - 'Col. Hughes', - 'Pvt. Adams', - 'Cpl. Fisher', - 'Sgt. Kelly', - 'Lt. Ramirez', - 'Capt. Hayes', - 'Maj. Black', - 'Col. Stone', - 'Pvt. Reed', - 'Cpl. Diaz', - 'Sgt. Jenkins', - 'Lt. Barnes', - 'Capt. Morgan', - 'Maj. Torres', - 'Col. Scott', - 'Pvt. Butler', - 'Cpl. Perry', - 'Sgt. Ramirez', - 'Lt. Kelly', - 'Capt. Palmer', - 'Maj. Wood', - 'Col. Cox', - 'Pvt. Price', - 'Cpl. Hayes', - 'Sgt. Foster', - 'Lt. Simmons', - 'Capt. Reed', - 'Maj. Morris', - 'Col. Gray', - 'Pvt. Nelson' -} +local NameProvider = require("__shared/Names/NameProvider") ----@class BotNames -RUMilitaryNames = { - 'Pvt. Ivanov', - 'Cpl. Petrov', - 'Sgt. Sokolov', - 'Lt. Smirnov', - 'Capt. Orlov', - 'Maj. Vasiliev', - 'Col. Egorov', - 'Pvt. Dmitriev', - 'Cpl. Ivanova', - 'Sgt. Kuznetsov', - 'Lt. Alekseev', - 'Capt. Nikiforov', - 'Maj. Fedorov', - 'Col. Romanov', - 'Pvt. Mikhailov', - 'Cpl. Zaitsev', - 'Sgt. Fomin', - 'Lt. Stepanov', - 'Capt. Volkov', - 'Maj. Belikov', - 'Col. Dmitriev', - 'Pvt. Sergeyev', - 'Cpl. Solovyov', - 'Sgt. Voronin', - 'Lt. Vinnikov', - 'Capt. Semyonov', - 'Maj. Gromov', - 'Col. Tarasov', - 'Pvt. Vasilev', - 'Cpl. Alekhin', - 'Sgt. Yefimov', - 'Lt. Filippov', - 'Capt. Zorin', - 'Maj. Tikhonov', - 'Col. Kolesnikov', - 'Pvt. Antipov', - 'Cpl. Arkhipov', - 'Sgt. Raskolnikov', - 'Lt. Shcherbakov', - 'Capt. Ponomarev', - 'Maj. Korolev', - 'Col. Ivanovich', - 'Pvt. Sidorov', - 'Cpl. Makarov', - 'Sgt. Grigoryev', - 'Lt. Timofeyev', - 'Capt. Dubrovsky', - 'Maj. Yermakov', - 'Col. Kravtsov', - 'Pvt. Alekseeva', - 'Cpl. Baranov', - 'Sgt. Savin', - 'Lt. Morozov', - 'Capt. Sorokin', - 'Maj. Zhukov', - 'Col. Karpov', - 'Pvt. Shubin', - 'Cpl. Cherepanov', - 'Sgt. Melnikov', - 'Lt. Larkin', - 'Capt. Shishkin', - 'Maj. Golubev', - 'Col. Avdeyev', - 'Pvt. Kuzmina' -} +-- Build pool once per extension load; shuffle internally +NameProvider.Init() + +local generic, us, ru = NameProvider.ExportLegacyTables() + +-- Fun-Bots expects these globals to exist: +BotNames = generic +USMarinesNames = us +RUMilitaryNames= ru + +return BotNames diff --git a/ext/Shared/Names/NameProvider.lua b/ext/Shared/Names/NameProvider.lua new file mode 100644 index 00000000..ae875a14 --- /dev/null +++ b/ext/Shared/Names/NameProvider.lua @@ -0,0 +1,251 @@ +-- ext/Shared/Names/NameProvider.lua +-- Code by: JMDigital (https://github.com/JenkinsTR) +-- Modular name provider with pack mixing, uniqueness, and legacy exports. + +local M = {} + +-- Try to read Fun-Bots token for optional stripping +local BOT_TOKEN = "" +pcall(function() + local ok, reg = pcall(require, "__shared/Registry/Registry") + if ok and reg and reg.COMMON and reg.COMMON.BOT_TOKEN ~= nil then + BOT_TOKEN = reg.COMMON.BOT_TOKEN or "" + end +end) + +-- ---------------- User configuration ---------------- +-- Choose packs and weights. Higher weight = more frequent. +-- Add/remove packs by adding a require below. +local PACK_CATALOG = { + ["global_default"] = function() return require("__shared/Names/packs/global_default") end, + ["au"] = function() return require("__shared/Names/packs/au") end, + ["nz"] = function() return require("__shared/Names/packs/nz") end, + ["uk"] = function() return require("__shared/Names/packs/uk") end, + ["ca"] = function() return require("__shared/Names/packs/ca") end, + ["mil_us"] = function() return require("__shared/Names/packs/mil_us") end, + ["mil_ru"] = function() return require("__shared/Names/packs/mil_ru") end, + ["gamer_tags"] = function() return require("__shared/Names/packs/gamer_tags") end, +} + +-- Active mix (order doesn’t matter) +local ACTIVE_PACKS = { + { id = "global_default", weight = 0.6 }, + { id = "gamer_tags", weight = 0.25 }, + { id = "au", weight = 0.35 }, + { id = "nz", weight = 0.25 }, + { id = "uk", weight = 0.25 }, + { id = "ca", weight = 0.25 }, + -- keep mil packs out of the general pool if you only want them for team-flavour +} + +-- Behaviour toggles +local OPTIONS = { + stripClanTags = false, -- keep [AU] etc. visible in names + stripFunBotsToken = true, -- drop BOT_TOKEN if present + enforceAscii = true, -- keep overlay clean + maxUnified = 120, -- cap total exported names (for BotNames legacy) + seedWithTeamFlavour = true, -- US/RU team-specific defaults for legacy exports +} + +-- -------- Regional tag policy (runtime; not stored in packs) -------- +-- Add/remove regions as you add chatter packs. Weights = relative frequency. +local REGION_TAGS = { + { tag = "[AU]", weight = 1.0, pack = "AU" }, + { tag = "[NZ]", weight = 0.9, pack = "NZ" }, + { tag = "[UK]", weight = 0.8, pack = "UK" }, + { tag = "[CA]", weight = 0.8, pack = "CA" }, + { tag = "[US]", weight = 1.0, pack = "Default" }, +} + +-- Probability a *new* name is tagged. Tune to taste. +local TAG_APPLY_PROB = 0.35 + +-- Prevent tagging names that *already* have a visible clan tag like [CLAN] +local function has_leading_tag(n) + if not n then return false end + -- fast path: first non-space must be bracket + if not n:match("^%s*[%[%(%{]") then return false end + -- then confirm a balanced token of one of the bracket types + return n:match("^%s*%b[]") or n:match("^%s*%b()") or n:match("^%s*%b{}") +end + +local function pick_weighted(list) + local total = 0 + for _, r in ipairs(list) do total = total + (r.weight or 0) end + if total <= 0 then return nil end + local roll = math.random() * total + local acc = 0 + for _, r in ipairs(list) do + acc = acc + (r.weight or 0) + if roll <= acc then return r end + end + return list[#list] +end + +local function maybe_tag(name) + -- never tag if it already starts with any bracketed token + if has_leading_tag(name) then return name end + if math.random() >= TAG_APPLY_PROB then return name end + local region = pick_weighted(REGION_TAGS) + if not region or not region.tag then return name end + return region.tag .. name +end + +-- ---------------- Helpers ---------------- + +local function sanitize_ascii(s) + if not s then return "" end + s = tostring(s) + -- replace fancy quotes/dashes/ellipsis + s = s:gsub("\226\128\153", "'"):gsub("\226\128\156", "\""):gsub("\226\128\157", "\"") + s = s:gsub("\226\128\166", "..."):gsub("\226\128[\145\146\147\148]", "-") + s = s:gsub("[^\32-\126]", "") -- drop other non-ASCII + return s +end + +local function strip_clan_tag(name) + if not name then return "" end + -- remove one leading [TAG]/(TAG)/{TAG}, then trailing whitespace + -- NOTE: %s* is OUTSIDE the class so it's real whitespace, not literal 's'/'*' + local stripped = name:gsub("^%s*[%(%[{][^%]%)}]+[%]%)}%s*", "") + return stripped:match("^%s*(.-)%s*$") or stripped +end + +local function strip_bot_token(name) + if BOT_TOKEN == nil or BOT_TOKEN == "" then return name end + if name:sub(1, #BOT_TOKEN) == BOT_TOKEN then + return name:sub(#BOT_TOKEN + 1) + end + return name +end + +local function clean_name(raw) + local n = raw or "" + if OPTIONS.stripFunBotsToken then n = strip_bot_token(n) end + if OPTIONS.stripClanTags then n = strip_clan_tag(n) end + if OPTIONS.enforceAscii then n = sanitize_ascii(n) end + -- collapse double spaces + n = n:gsub("%s%s+", " ") + return n +end + +-- ---------------- Pack loading & mixing ---------------- + +local loadedPacks = {} +local unifiedPool = {} -- array of names +local uniqueSet = {} -- set for fast dup checks + +local function load_pack(id) + if loadedPacks[id] then return loadedPacks[id] end + local fn = PACK_CATALOG[id] + if not fn then return { id = id, names = {}, meta = {} } end + local p = fn() + -- pack structure: { id="au_nz", names={...}, meta={team="US"/"RU"/nil}} + if not p or type(p) ~= "table" then + p = { id = id, names = {}, meta = {} } + end + p.id = p.id or id + p.meta = p.meta or {} + p.names= p.names or {} + loadedPacks[id] = p + return p +end + +local function push_unique(raw) + local n = clean_name(raw) + if n == "" then return end + if uniqueSet[n] then return end + uniqueSet[n] = true + unifiedPool[#unifiedPool + 1] = n +end + +local function build_unified_pool() + loadedPacks, unifiedPool, uniqueSet = {}, {}, {} + local LIMIT = OPTIONS.maxUnified or 120 + + -- weighted copy pass + for _, ref in ipairs(ACTIVE_PACKS) do + local pack = load_pack(ref.id) + local w = math.max(0, ref.weight or 1) + + if w > 0 and #pack.names > 0 then + -- naive weighting: repeat sampling proportional to weight * size + local budget = math.ceil(w * math.max(20, math.floor(#pack.names * 0.5))) + for i = 1, budget do + local name = pack.names[((i - 1) % #pack.names) + 1] + name = maybe_tag(name) -- inject a regional tag sometimes + push_unique(name) + if #unifiedPool >= LIMIT then break end + end + end + + if #unifiedPool >= LIMIT then break end + end + + -- if pool undersized, backfill with any remaining pack content + if #unifiedPool < LIMIT then + for id, _ in pairs(PACK_CATALOG) do + local p = load_pack(id) + for _, n in ipairs(p.names) do + push_unique(maybe_tag(n)) + if #unifiedPool >= LIMIT then break end + end + if #unifiedPool >= LIMIT then break end + end + end +end + +-- For session unique “next name” +local cursor = 1 +local function shuffle_in_place(t) + for i = #t, 2, -1 do + local j = math.random(i) + t[i], t[j] = t[j], t[i] + end +end + +-- ---------------- Public API ---------------- + +function M.Init(seed) + if seed then math.randomseed(seed) else math.randomseed(os.time() % 2147483647) end + build_unified_pool() + shuffle_in_place(unifiedPool) + cursor = 1 +end + +function M.NextName() + if #unifiedPool == 0 then M.Init() end + local n = unifiedPool[cursor] + cursor = cursor + 1 + if cursor > #unifiedPool then cursor = 1 end + return n or ("Player"..tostring(math.random(10000,99999))) +end + +-- Legacy exports for Fun-Bots, so we remain compatible +function M.ExportLegacyTables() + if #unifiedPool == 0 then M.Init() end + + -- generic names pool + local generic = {} + for i = 1, math.min(#unifiedPool, OPTIONS.maxUnified or 120) do + generic[#generic + 1] = unifiedPool[i] + end + + -- team-flavoured defaults for US/RU (override via packs) + local us = load_pack("mil_us").names + local ru = load_pack("mil_ru").names + + -- fallbacks if empty + if not us or #us == 0 then us = { "Pvt. Walker", "Cpl. Nguyen", "Sgt. Patel" } end + if not ru or #ru == 0 then ru = { "Pvt. Ivanov", "Cpl. Petrov", "Sgt. Sokolov" } end + + -- sanitize/strip these too + local USMar = {} + for _, n in ipairs(us) do USMar[#USMar + 1] = clean_name(n) end + local RUMil = {} + for _, n in ipairs(ru) do RUMil[#RUMil + 1] = clean_name(n) end + + return generic, USMar, RUMil +end + +return M diff --git a/ext/Shared/Names/packs/au.lua b/ext/Shared/Names/packs/au.lua new file mode 100644 index 00000000..3a3da69a --- /dev/null +++ b/ext/Shared/Names/packs/au.lua @@ -0,0 +1,33 @@ +-- ext/Shared/Names/packs/au.lua +-- Code by: JMDigital (https://github.com/JenkinsTR) +-- Australia pack: realistic given names, light nicknames, subtle place/fauna handles. ASCII only. +return { + id = "au", + meta = { region = "au" }, + names = { + -- Nicknames / handles + "Jono","Dazza","Shano","Macca","Gaz","Hendo","Sutto","Browny","Wazza","Bluey","Bazza","Shazza", + "Kevvy","Nath","Stevo","Juzzy","Moz","Cambo","Chambo","Chazza","Dyl","Hughsey","Sparky","Chook", + "Bozza","Muzza","Roo","Swanny","Tex","Griffo","Richo","Nugget","Tigsy","Wilko","Barnsey","Barker", + "Davo","Kez","Coops","Bucks","Paddy","Shep","Hobbo","Noods","Snowy","Smoko","Rusty", + + -- Common AU given names + "Lachlan","Declan","Callum","Hamish","Angus","Cooper","Mason","Hunter","Blake","Zane", + "Aiden","Kane","Bailey","Jayden","Kieran","Corey","Bradley","Mitchell","Brayden","Riley", + "Logan","Jordan","Cody","Harrison","Asher","Felix","Xavier","Quinn","Flynn","Archer", + "Harvey","Leo","Miles","Fraser","Beau","Joel","Toby","Rhys","Dante","Oscar", + "Lincoln","Spencer","Kobe","Eli","Hugo","Nixon","Harley","Rafe","Jonah","Nate", + "Ari","Zeke","Kai","Micah","Noah","Levi","Ollie","Max","Jai","Tate", + + -- Place-flavoured (subtle; not bracket tags) + "SydneySid","BrissyBryce","PerthPete","AdelaideAsh","CanberraCam","DarwinDrew","FreoFinn", + "NullarborNash","PilbaraPip","KimberleyKai","OutbackOwen","KakaduKen","GibberGabe", + + -- Tasmanian nods (you’re in Tas) + "HentyHarry","DerwentDax","TamarToby","HuonHugh","UlverstoneUli","DevonportDev","CradleCraig", + + -- Fauna / bush + "MagpieMike","KookaburraKev","WedgetailWill","WombatWade","QuokkaQuinn","GoannaGus", + "BanksiaBen","IronbarkIan","NumbatNate","RockWallabyRex", + } +} diff --git a/ext/Shared/Names/packs/ca.lua b/ext/Shared/Names/packs/ca.lua new file mode 100644 index 00000000..490ff8d6 --- /dev/null +++ b/ext/Shared/Names/packs/ca.lua @@ -0,0 +1,24 @@ +-- ext/Shared/Names/packs/ca.lua +-- Code by: JMDigital (https://github.com/JenkinsTR) +-- Canada pack: English + French-leaning given names (ASCII), subtle place/nature handles. No bracket tags. +return { + id = "ca", + meta = { region = "ca" }, + names = { + -- Common CA given names (EN/FR mix, ASCII) + "Liam","Noah","William","James","Benjamin","Logan","Lucas","Jackson","Ethan","Jacob","Oliver", + "Daniel","Alexander","Nathan","Michael","Matthew","Ryan","Tyler","Connor","Dylan","Aiden", + "Gavin","Owen","Eli","Caleb","Mason","Leo","Max","Theo","Henry","Thomas","Samuel","Gabriel", + "Simon","Marc","Andre","Etienne","Luc","Louis","Sebastien","Olivier","Mathieu","Antoine", + "Hugo","Pierre","Xavier","Nicolas","Alexandre","Julien","Laurent", + + -- Place-flavoured (light) + "TorontoTom","MontrealMax","QuebecQuinn","OttawaOwen","CalgaryCam","EdmontonEli","VancouverVic", + "WinnipegWes","SaskatoonSas","ReginaRex","HalifaxHank","HamiltonHale","KitchenerKit","LondonONLeo", + "WaterlooWalt","GatineauGabe","SherbrookeShae","TroisRivieresTroy","SudburySid","StJohnsJack", + + -- Nature / symbols + "MapleMitch","LoonLuke","MooseMason","TimberTy","PrairiePaul","RockiesRafe","LaurentianLeo", + "MaritimesMax","CabotCory","GreatLakesGabe","TundraTom","BorealBen","CanoeKyle","PoutinePete", + } +} diff --git a/ext/Shared/Names/packs/gamer_tags.lua b/ext/Shared/Names/packs/gamer_tags.lua new file mode 100644 index 00000000..f3504f6c --- /dev/null +++ b/ext/Shared/Names/packs/gamer_tags.lua @@ -0,0 +1,30 @@ +-- ext/Shared/Names/packs/gamer_tags.lua +-- Code by: JMDigital (https://github.com/JenkinsTR) +-- Global gamer tags +return { + id = "gamer_tags", + meta = { region = "global" }, + names = { + "ClutchKing", "WallBangPro", "HipFireHero", "SprayNPray", "PixelPwner", "NoScopeAndy", + "RektMachine", "LagHunter", "QuickScopez", "KnifeOnly", "TriggerHappy", "SpawnPeek", + "MapControl", "PointMan", "Digger", "Kiwi", "Predador", "Viking", + "Panzer", "HeadshotHarry", "FragQueen", "PeekAdvantage", "DeltaPeak", "StrafeGod", + "TapTap", "EntryFrag", "TopFrag", "AceClutcher", "LurkMaster", "IGL_Mind", "EcoSaver", + "UtilityNinja", "FlashbangFrank", "MollyMaker", "SmokeLineup", "FlickWizard", "AimBotOff", + "HighPingHero", "LowSensLarry", "RecoilTamer", "ADSfan", "CrouchSpammer", "SlideCancel", + "BunnyHopper", "WideSwing", "PreFirePro", "HardpointHero", "PayloadPusher", "DominationDuke", + "RushB", "DefuseKit", "PlantNPray", "NoRecoil", "FullBuy", "EcoRound", "SaveTheOp", + "HeadGlitcher", "CornerCamper", "WindowWatcher", "RotateMaster", "CrossfireKing", "InfoTrader", + "AngleHolder", "PixelAngle", "SoundCue", "RadarSense", "NadeChef", "WallbangWizard", + "TimeThief", "ZoneBreaker", "ObjectiveAndy", "SpawnTrap", "TopTierTact", "SweatLord", + "FlickshotFae", "BurstFire", "TapHead", "OneDeag", "AKEnjoyer", "M4Main", "ShottySensei", + "SniperSavage", "DMR_Duke", "PistolPete", "SMG_Sage", "LMG_Lug", "RifleRonin", "ScoutSeer", + "KDAKeeper", "DamageDealer", "SupportMain", "MedicMark", "EngineerEz", "ReconRogue", + "AssaultAndy", "PilotPog", "TankerTony", "HeliHero", "JetJockey", "BoatBandit", "MapKnowledge", + "CalloutCarl", "MacroMind", "MicroMoves", "LineupLarry", "OffAngle", "FlankFiend", + "CrosshairZen", "MiracleRound", "LastAlive", "NinjaDefuse", "FakePlant", "TiltProof", + "ClutchGene", "MentalBoom", "WKeyWarrior", "HoldW", "HardCarry", "Nomad_77", "Specter_13", + "Vector_404", "Phantom_9", "Cipher_21", "Rogue_88", "Valk_17", "Sable_23", "Tracer_12", + "Stone_45", "Hawke_06", + } +} diff --git a/ext/Shared/Names/packs/global_default.lua b/ext/Shared/Names/packs/global_default.lua new file mode 100644 index 00000000..5ba51a7e --- /dev/null +++ b/ext/Shared/Names/packs/global_default.lua @@ -0,0 +1,27 @@ +-- ext/Shared/Names/packs/global_default.lua +-- Code by: JMDigital (https://github.com/JenkinsTR) +-- Short, readable handles suitable for any region. +return { + id = "global_default", + meta = { region = "global" }, + names = { + "Alex", "Ben", "Chris", "Drew", "Eli", "Finn", "Gabe", "Hugo", "Ivan", "Jake", "Kyle", "Luke", + "Matt", "Nate", "Owen", "Ryan", "Sam", "Tom", "Zac", "Oli", "Maverick", "Stone", "Hawke", + "Specter", "Valk", "Rogue", "Nomad", "Archer", "Tracer", "Cipher", "Vector", "Sable", + "Phantom", "Liam", "Noah", "Mia", "Anna", "Mila", "Luca", "Leo", "Max", "Theo", "Zoe", "Nora", + "Eva", "Sofia", "Clara", "Iris", "Ada", "June", "Skye", "Remy", "Jules", "Omar", "Amir", + "Yusuf", "Ali", "Zain", "Hadi", "Khalid", "Sami", "Karim", "Nabil", "Diego", "Mateo", "Sergio", + "Pablo", "Marco", "Enzo", "Nico", "Rafa", "Luis", "Mario", "Viktor", "Misha", "Dima", "Yuri", + "Andrei", "Nikita", "Roman", "Oleg", "Igor", "Sasha", "Ken", "Ryu", "Hiro", "Sora", "Ren", + "Yuki", "Aki", "Kyo", "Taro", "Shin", "Arun", "Ravi", "Vik", "Aadi", "Ishan", "Dev", "Neel", + "Rohan", "Kabir", "Veer", "Jon", "Will", "Sean", "Mark", "Paul", "Eric", "Todd", "Gary", + "Neil", "Troy", "Bryce", "Shawn", "Dylan", "Ethan", "Cameron", "Logan", "Tyler", "Jason", + "Scott", "Grant", "Maya", "Aria", "Lara", "Lina", "Elena", "Sara", "Nina", "Mina", "Keira", + "Tara", "Oskar", "Marek", "Jan", "Tomas", "Milan", "Jiri", "Adam", "Petr", "Luka", "Filip", + "Kaan", "Emre", "Can", "Burak", "Umut", "Eren", "Deniz", "Arda", "Kuzey", "Onur", "Yohan", + "Sven", "Lars", "Johan", "Bjorn", "Ola", "Kurt", "Emil", "Otto", "Karl", "Ares", "Atlas", + "Blitz", "Viper", "Raptor", "Shadow", "Echo", "Nova", "Drift", "Forge", "Quartz", "Onyx", + "Flint", "Grit", "Bolt", "Ghost", "Gale", "Frost", "Ash", "Flare", "Rex", "Ace", "Zen", "Kai", + "Jett", "Zed", "Rin", "Ashen", "Slate", "Vale", + } +} diff --git a/ext/Shared/Names/packs/mil_ru.lua b/ext/Shared/Names/packs/mil_ru.lua new file mode 100644 index 00000000..e86c2fda --- /dev/null +++ b/ext/Shared/Names/packs/mil_ru.lua @@ -0,0 +1,42 @@ +-- ext/Shared/Names/packs/mil_ru.lua +-- Code by: JMDigital (https://github.com/JenkinsTR) +-- Russian-style surnames paired with common ranks; ASCII only. +return { + id = "mil_ru", + meta = { team = "RU" }, + names = { + "Pvt. Ivanov", "Cpl. Ivanov", "Sgt. Ivanov", "Lt. Ivanov", "Capt. Ivanov", "Maj. Ivanov", + "Col. Ivanov", "Pvt. Petrov", "Cpl. Petrov", "Sgt. Petrov", "Lt. Petrov", "Capt. Petrov", + "Maj. Petrov", "Col. Petrov", "Pvt. Sidorov", "Cpl. Sidorov", "Sgt. Sidorov", "Lt. Sidorov", + "Capt. Sidorov", "Maj. Sidorov", "Col. Sidorov", "Pvt. Smirnov", "Cpl. Smirnov", + "Sgt. Smirnov", "Lt. Smirnov", "Capt. Smirnov", "Maj. Smirnov", "Col. Smirnov", + "Pvt. Kuznetsov", "Cpl. Kuznetsov", "Sgt. Kuznetsov", "Lt. Kuznetsov", "Capt. Kuznetsov", + "Maj. Kuznetsov", "Col. Kuznetsov", "Pvt. Popov", "Cpl. Popov", "Sgt. Popov", "Lt. Popov", + "Capt. Popov", "Maj. Popov", "Col. Popov", "Pvt. Sokolov", "Cpl. Sokolov", "Sgt. Sokolov", + "Lt. Sokolov", "Capt. Sokolov", "Maj. Sokolov", "Col. Sokolov", "Pvt. Lebedev", "Cpl. Lebedev", + "Sgt. Lebedev", "Lt. Lebedev", "Capt. Lebedev", "Maj. Lebedev", "Col. Lebedev", "Pvt. Morozov", + "Cpl. Morozov", "Sgt. Morozov", "Lt. Morozov", "Capt. Morozov", "Maj. Morozov", "Col. Morozov", + "Pvt. Volkov", "Cpl. Volkov", "Sgt. Volkov", "Lt. Volkov", "Capt. Volkov", "Maj. Volkov", + "Col. Volkov", "Pvt. Fedorov", "Cpl. Fedorov", "Sgt. Fedorov", "Lt. Fedorov", "Capt. Fedorov", + "Maj. Fedorov", "Col. Fedorov", "Pvt. Pavlov", "Cpl. Pavlov", "Sgt. Pavlov", "Lt. Pavlov", + "Capt. Pavlov", "Maj. Pavlov", "Col. Pavlov", "Pvt. Egorov", "Cpl. Egorov", "Sgt. Egorov", + "Lt. Egorov", "Capt. Egorov", "Maj. Egorov", "Col. Egorov", "Pvt. Vasiliev", "Cpl. Vasiliev", + "Sgt. Vasiliev", "Lt. Vasiliev", "Capt. Vasiliev", "Maj. Vasiliev", "Col. Vasiliev", + "Pvt. Mikhailov", "Cpl. Mikhailov", "Sgt. Mikhailov", "Lt. Mikhailov", "Capt. Mikhailov", + "Maj. Mikhailov", "Col. Mikhailov", "Pvt. Andreev", "Cpl. Andreev", "Sgt. Andreev", + "Lt. Andreev", "Capt. Andreev", "Maj. Andreev", "Col. Andreev", "Pvt. Alexandrov", + "Cpl. Alexandrov", "Sgt. Alexandrov", "Lt. Alexandrov", "Capt. Alexandrov", "Maj. Alexandrov", + "Col. Alexandrov", "Pvt. Nikolaev", "Cpl. Nikolaev", "Sgt. Nikolaev", "Lt. Nikolaev", + "Capt. Nikolaev", "Maj. Nikolaev", "Col. Nikolaev", "Pvt. Orlov", "Cpl. Orlov", "Sgt. Orlov", + "Lt. Orlov", "Capt. Orlov", "Maj. Orlov", "Col. Orlov", "Pvt. Belov", "Cpl. Belov", + "Sgt. Belov", "Lt. Belov", "Capt. Belov", "Maj. Belov", "Col. Belov", "Pvt. Medvedev", + "Cpl. Medvedev", "Sgt. Medvedev", "Lt. Medvedev", "Capt. Medvedev", "Maj. Medvedev", + "Col. Medvedev", "Pvt. Antonov", "Cpl. Antonov", "Sgt. Antonov", "Lt. Antonov", + "Capt. Antonov", "Maj. Antonov", "Col. Antonov", "Pvt. Tarasov", "Cpl. Tarasov", + "Sgt. Tarasov", "Lt. Tarasov", "Capt. Tarasov", "Maj. Tarasov", "Col. Tarasov", "Pvt. Gromov", + "Cpl. Gromov", "Sgt. Gromov", "Lt. Gromov", "Capt. Gromov", "Maj. Gromov", "Col. Gromov", + "Pvt. Zaitsev", "Cpl. Zaitsev", "Sgt. Zaitsev", "Lt. Zaitsev", "Capt. Zaitsev", "Maj. Zaitsev", + "Col. Zaitsev", "Pvt. Vinogradov", "Cpl. Vinogradov", "Sgt. Vinogradov", "Lt. Vinogradov", + "Capt. Vinogradov", + } +} diff --git a/ext/Shared/Names/packs/mil_us.lua b/ext/Shared/Names/packs/mil_us.lua new file mode 100644 index 00000000..a917cd44 --- /dev/null +++ b/ext/Shared/Names/packs/mil_us.lua @@ -0,0 +1,45 @@ +-- ext/Shared/Names/packs/mil_us.lua +-- Code by: JMDigital (https://github.com/JenkinsTR) +-- US surnames paired with common ranks; wide surname coverage. +return { + id = "mil_us", + meta = { team = "US" }, + names = { + "Pvt. Jackson", "Cpl. Jackson", "Sgt. Jackson", "Lt. Jackson", "Capt. Jackson", "Maj. Jackson", + "Col. Jackson", "Pvt. Ramirez", "Cpl. Ramirez", "Sgt. Ramirez", "Lt. Ramirez", "Capt. Ramirez", + "Maj. Ramirez", "Col. Ramirez", "Pvt. Thompson", "Cpl. Thompson", "Sgt. Thompson", + "Lt. Thompson", "Capt. Thompson", "Maj. Thompson", "Col. Thompson", "Pvt. Bennett", + "Cpl. Bennett", "Sgt. Bennett", "Lt. Bennett", "Capt. Bennett", "Maj. Bennett", "Col. Bennett", + "Pvt. Fisher", "Cpl. Fisher", "Sgt. Fisher", "Lt. Fisher", "Capt. Fisher", "Maj. Fisher", + "Col. Fisher", "Pvt. Carter", "Cpl. Carter", "Sgt. Carter", "Lt. Carter", "Capt. Carter", + "Maj. Carter", "Col. Carter", "Pvt. Reynolds", "Cpl. Reynolds", "Sgt. Reynolds", + "Lt. Reynolds", "Capt. Reynolds", "Maj. Reynolds", "Col. Reynolds", "Pvt. Nguyen", + "Cpl. Nguyen", "Sgt. Nguyen", "Lt. Nguyen", "Capt. Nguyen", "Maj. Nguyen", "Col. Nguyen", + "Pvt. Patel", "Cpl. Patel", "Sgt. Patel", "Lt. Patel", "Capt. Patel", "Maj. Patel", + "Col. Patel", "Pvt. Wells", "Cpl. Wells", "Sgt. Wells", "Lt. Wells", "Capt. Wells", + "Maj. Wells", "Col. Wells", "Pvt. Smith", "Cpl. Smith", "Sgt. Smith", "Lt. Smith", + "Capt. Smith", "Maj. Smith", "Col. Smith", "Pvt. Johnson", "Cpl. Johnson", "Sgt. Johnson", + "Lt. Johnson", "Capt. Johnson", "Maj. Johnson", "Col. Johnson", "Pvt. Williams", + "Cpl. Williams", "Sgt. Williams", "Lt. Williams", "Capt. Williams", "Maj. Williams", + "Col. Williams", "Pvt. Brown", "Cpl. Brown", "Sgt. Brown", "Lt. Brown", "Capt. Brown", + "Maj. Brown", "Col. Brown", "Pvt. Jones", "Cpl. Jones", "Sgt. Jones", "Lt. Jones", + "Capt. Jones", "Maj. Jones", "Col. Jones", "Pvt. Miller", "Cpl. Miller", "Sgt. Miller", + "Lt. Miller", "Capt. Miller", "Maj. Miller", "Col. Miller", "Pvt. Davis", "Cpl. Davis", + "Sgt. Davis", "Lt. Davis", "Capt. Davis", "Maj. Davis", "Col. Davis", "Pvt. Garcia", + "Cpl. Garcia", "Sgt. Garcia", "Lt. Garcia", "Capt. Garcia", "Maj. Garcia", "Col. Garcia", + "Pvt. Rodriguez", "Cpl. Rodriguez", "Sgt. Rodriguez", "Lt. Rodriguez", "Capt. Rodriguez", + "Maj. Rodriguez", "Col. Rodriguez", "Pvt. Martinez", "Cpl. Martinez", "Sgt. Martinez", + "Lt. Martinez", "Capt. Martinez", "Maj. Martinez", "Col. Martinez", "Pvt. Hernandez", + "Cpl. Hernandez", "Sgt. Hernandez", "Lt. Hernandez", "Capt. Hernandez", "Maj. Hernandez", + "Col. Hernandez", "Pvt. Lopez", "Cpl. Lopez", "Sgt. Lopez", "Lt. Lopez", "Capt. Lopez", + "Maj. Lopez", "Col. Lopez", "Pvt. Gonzalez", "Cpl. Gonzalez", "Sgt. Gonzalez", "Lt. Gonzalez", + "Capt. Gonzalez", "Maj. Gonzalez", "Col. Gonzalez", "Pvt. Wilson", "Cpl. Wilson", + "Sgt. Wilson", "Lt. Wilson", "Capt. Wilson", "Maj. Wilson", "Col. Wilson", "Pvt. Anderson", + "Cpl. Anderson", "Sgt. Anderson", "Lt. Anderson", "Capt. Anderson", "Maj. Anderson", + "Col. Anderson", "Pvt. Thomas", "Cpl. Thomas", "Sgt. Thomas", "Lt. Thomas", "Capt. Thomas", + "Maj. Thomas", "Col. Thomas", "Pvt. Taylor", "Cpl. Taylor", "Sgt. Taylor", "Lt. Taylor", + "Capt. Taylor", "Maj. Taylor", "Col. Taylor", "Pvt. Moore", "Cpl. Moore", "Sgt. Moore", + "Lt. Moore", "Capt. Moore", "Maj. Moore", "Col. Moore", "Pvt. Martin", "Cpl. Martin", + "Sgt. Martin", "Lt. Martin", + } +} diff --git a/ext/Shared/Names/packs/nz.lua b/ext/Shared/Names/packs/nz.lua new file mode 100644 index 00000000..25e9a882 --- /dev/null +++ b/ext/Shared/Names/packs/nz.lua @@ -0,0 +1,24 @@ +-- ext/Shared/Names/packs/nz.lua +-- Code by: JMDigital (https://github.com/JenkinsTR) +-- New Zealand pack: mix of common NZ given names + light Māori/Pasifika (ASCII), subtle place handles. No bracket tags. +return { + id = "nz", + meta = { region = "nz" }, + names = { + -- Everyday / short handles + "KiwiKev","Jordy","Tane","Reuben","Beaudy","Rangi","Tui","Niko","Ashcroft","Bicko","Hammond", + "Westie","DunnerDan","Noah","Liam","Jack","Oliver","Lucas","Hunter","Riley","Logan","Cooper", + "Levi","Mason","Archer","Theo","Felix","Xavier","Asher","Quinn","Flynn","Blake","Leo","Miles", + "Fraser","Joel","Toby","Rhys","Oscar","Spencer","Eli","Hugo","Harley","Jonah","Nate","Ari","Kai", + "Micah","Max","Tate","Zac","Sam","Ben","Nick","Josh","Tom", + + -- Māori / Pasifika (ASCII only) + "Tama","Manaia","Kahu","Wiremu","Hemi","Rawiri","Nikau","Taika","Kaia","Aroha","Kiri","Moana", + "Rongo","Tiare","Pania","Whetu","Hine","Manu","Maia","Ariki","Kauri","Tane","Rangi","Hana","Mere", + + -- Place-flavoured (subtle) + "AucklandAce","WellyWills","RotoruaRex","TaupoTroy","WanakaWade","NelsonNash","TaranakiTom", + "CoromandelCory","QueenstownQ","BayOfPlentyBen","SouthlandSam","OtagoOwen","CanterburyCam", + "ManawatuMax","HawkesBayHawk", + } +} diff --git a/ext/Shared/Names/packs/uk.lua b/ext/Shared/Names/packs/uk.lua new file mode 100644 index 00000000..1120ab01 --- /dev/null +++ b/ext/Shared/Names/packs/uk.lua @@ -0,0 +1,23 @@ +-- ext/Shared/Names/packs/uk.lua +-- Code by: JMDigital (https://github.com/JenkinsTR) +-- UK pack: contemporary UK given names + light nicknames and tasteful locality/animal calls. ASCII only. +return { + id = "uk", + meta = { region = "uk" }, + names = { + -- Nicknames / short + "Alfie","Archie","Harry","Jack","George","Oliver","Freddie","Theo","Finlay","Callum","Lewis", + "Jamie","Charlie","Ben","Tom","Sam","Joe","Reece","Harvey","Tyler","Dylan","Liam","Ethan", + "Oscar","Leo","Max","Toby","Ollie","Henry","Arthur","Nathan","Luke","Alex","Mason","Jay", + "Gaz","Baz","Kev","Ste","Jez","Loz","Tez", + + -- Place-flavoured (subtle; avoid stereotypes) + "LondonLeo","ManchesterMax","BristolBen","LiverpoolLiam","LeedsLuke","KentKurt","SussexSam", + "EssexEli","SurreySeb","DevonDave","CornwallCam","NorfolkNate","SomersetSol","SuffolkSeth", + "WessexWes","YorkshireYorke","HighlandsHal","CotswoldsCol","MerseysideMerrin","TyneTrent", + + -- Animal / nature calls common in UK + "Badger","RedFox","Otter","Heron","Kestrel","Buzzard","Peregrine","Stoat","Hedgehog","Rook", + "Wren","Robin","Pipit","Marten","Osprey","Cormorant", + } +} diff --git a/ext/Shared/__init__.lua b/ext/Shared/__init__.lua index 559c7213..1eeda07a 100644 --- a/ext/Shared/__init__.lua +++ b/ext/Shared/__init__.lua @@ -25,6 +25,8 @@ require('__shared/Constants/TeamSwitchModes') require('__shared/WeaponList') require('__shared/EbxEditUtils') +-- load the chat config +require('__shared/BotChatterConfig') ---@type Language local m_Language = require('__shared/Language')