Files
wnsrc/gamemodes/terrortown/gamemode/cl_search.lua
lifestorm 94063e4369 Upload
2024-08-04 22:55:00 +03:00

515 lines
14 KiB
Lua

--[[
| This file was obtained through the combined efforts
| of Madbluntz & Plymouth Antiquarian Society.
|
| Credits: lifestorm, Gregory Wayne Rossel JR.,
| Maloy, DrPepper10 @ RIP, Atle!
|
| Visit for more: https://plymouth.thetwilightzone.ru/
--]]
-- Body search popup
local T = LANG.GetTranslation
local PT = LANG.GetParamTranslation
local is_dmg = util.BitSet
local dtt = { search_dmg_crush = DMG_CRUSH, search_dmg_bullet = DMG_BULLET, search_dmg_fall = DMG_FALL,
search_dmg_boom = DMG_BLAST, search_dmg_club = DMG_CLUB, search_dmg_drown = DMG_DROWN, search_dmg_stab = DMG_SLASH,
search_dmg_burn = DMG_BURN, search_dmg_tele = DMG_SONIC, search_dmg_car = DMG_VEHICLE }
-- "From his body you can tell XXX"
local function DmgToText(d)
for k, v in pairs(dtt) do
if is_dmg(d, v) then
return T(k)
end
end
if is_dmg(d, DMG_DIRECT) then
return T("search_dmg_burn")
end
return T("search_dmg_other")
end
-- Info type to icon mapping
-- Some icons have different appearances based on the data value. These have a
-- separate table inside the TypeToMat table.
-- Those that have a lot of possible data values are defined separately, either
-- as a function or a table.
local dtm = { bullet = DMG_BULLET, rock = DMG_CRUSH, splode = DMG_BLAST, fall = DMG_FALL, fire = DMG_BURN }
local function DmgToMat(d)
for k, v in pairs(dtm) do
if is_dmg(d, v) then
return k
end
end
if is_dmg(d, DMG_DIRECT) then
return "fire"
else
return "skull"
end
end
local function WeaponToIcon(d)
local wep = util.WeaponForClass(d)
return wep and wep.Icon or "vgui/ttt/icon_nades"
end
local TypeToMat = {
nick="id",
words="halp",
eq_armor="armor",
eq_radar="radar",
eq_disg="disguise",
role={[ROLE_TRAITOR]="traitor", [ROLE_DETECTIVE]="det", [ROLE_INNOCENT]="inno"},
c4="code",
dmg=DmgToMat,
wep=WeaponToIcon,
head="head",
dtime="time",
stime="wtester",
lastid="lastid",
kills="list"
}
-- Accessor for better fail handling
local function IconForInfoType(t, data)
local base = "vgui/ttt/icon_"
local mat = TypeToMat[t]
if istable(mat) then
mat = mat[data]
elseif isfunction(mat) then
mat = mat(data)
end
if not mat then
mat = TypeToMat["nick"]
end
-- ugly special casing for weapons, because they are more likely to be
-- customized and hence need more freedom in their icon filename
if t != "wep" then
return base .. mat
else
return mat
end
end
function PreprocSearch(raw)
local search = {}
for t, d in pairs(raw) do
search[t] = {img=nil, text="", p=10}
if t == "nick" then
search[t].text = PT("search_nick", {player = d})
search[t].p = 1
search[t].nick = d
elseif t == "role" then
if d == ROLE_TRAITOR then
search[t].text = T("search_role_t")
elseif d == ROLE_DETECTIVE then
search[t].text = T("search_role_d")
else
search[t].text = T("search_role_i")
end
search[t].p = 2
elseif t == "words" then
if d != "" then
-- only append "--" if there's no ending interpunction
local final = string.match(d, "[\\.\\!\\?]$") != nil
search[t].text = PT("search_words", {lastwords = d .. (final and "" or "--.")})
end
elseif t == "eq_armor" then
if d then
search[t].text = T("search_armor")
search[t].p = 17
end
elseif t == "eq_disg" then
if d then
search[t].text = T("search_disg")
search[t].p = 18
end
elseif t == "eq_radar" then
if d then
search[t].text = T("search_radar")
search[t].p = 19
end
elseif t == "c4" then
if d > 0 then
search[t].text= PT("search_c4", {num = d})
end
elseif t == "dmg" then
search[t].text = DmgToText(d)
search[t].p = 12
elseif t == "wep" then
local wep = util.WeaponForClass(d)
local wname = wep and LANG.TryTranslation(wep.PrintName)
if wname then
search[t].text = PT("search_weapon", {weapon = wname})
end
elseif t == "head" then
if d then
search[t].text = T("search_head")
end
search[t].p = 15
elseif t == "dtime" then
if d != 0 then
local ftime = util.SimpleTime(d, "%02i:%02i")
search[t].text = PT("search_time", {time = ftime})
search[t].text_icon = ftime
search[t].p = 8
end
elseif t == "stime" then
if d > 0 then
local ftime = util.SimpleTime(d, "%02i:%02i")
search[t].text = PT("search_dna", {time = ftime})
search[t].text_icon = ftime
end
elseif t == "kills" then
local num = table.Count(d)
if num == 1 then
local vic = Entity(d[1])
local dc = d[1] == 0 -- disconnected
if dc or (IsValid(vic) and vic:IsPlayer()) then
search[t].text = PT("search_kills1", {player = (dc and "<Disconnected>" or vic:Nick())})
end
elseif num > 1 then
local txt = T("search_kills2") .. "\n"
local nicks = {}
for k, idx in ipairs(d) do
local vic = Entity(idx)
local dc = idx == 0
if dc or (IsValid(vic) and vic:IsPlayer()) then
table.insert(nicks, (dc and "<Disconnected>" or vic:Nick()))
end
end
local last = #nicks
txt = txt .. table.concat(nicks, "\n", 1, last)
search[t].text = txt
end
search[t].p = 30
elseif t == "lastid" then
if d and d.idx != 0 then
local ent = Entity(d.idx)
if IsValid(ent) and ent:IsPlayer() then
search[t].text = PT("search_eyes", {player = ent:Nick()})
search[t].ply = ent
end
end
else
-- Not matching a type, so don't display
search[t] = nil
end
-- anything matching a type but not given a text should be removed
if search[t] and search[t].text == "" then
search[t] = nil
end
-- if there's still something here, we'll be showing it, so find an icon
if search[t] then
search[t].img = IconForInfoType(t, d)
end
end
hook.Call("TTTBodySearchPopulate", nil, search, raw)
return search
end
-- Returns a function meant to override OnActivePanelChanged, which modifies
-- dactive and dtext based on the search information that is associated with the
-- newly selected panel
local function SearchInfoController(search, dactive, dtext)
return function(s, pold, pnew)
local t = pnew.info_type
local data = search[t]
if not data then
ErrorNoHalt("Search: data not found", t, data,"\n")
return
end
-- If wrapping is on, the Label's SizeToContentsY misbehaves for
-- text that does not need wrapping. I long ago stopped wondering
-- "why" when it comes to VGUI. Apply hack, move on.
dtext:GetLabel():SetWrap(#data.text > 50)
dtext:SetText(data.text)
dactive:SetImage(data.img)
end
end
local function ShowSearchScreen(search_raw)
local client = LocalPlayer()
if not IsValid(client) then return end
local m = 8
local bw, bh = 100, 25
local bw_large = 125
local w, h = 425, 260
local rw, rh = (w - m*2), (h - 25 - m*2)
local rx, ry = 0, 0
local rows = 1
local listw, listh = rw, (64 * rows + 6)
local listx, listy = rx, ry
ry = ry + listh + m*2
rx = m
local descw, desch = rw - m*2, 80
local descx, descy = rx, ry
ry = ry + desch + m
local butx, buty = rx, ry
local dframe = vgui.Create("DFrame")
dframe:SetSize(w, h)
dframe:Center()
dframe:SetTitle(T("search_title") .. " - " .. search_raw.nick or "???")
dframe:SetVisible(true)
dframe:ShowCloseButton(true)
dframe:SetMouseInputEnabled(true)
dframe:SetKeyboardInputEnabled(true)
dframe:SetDeleteOnClose(true)
dframe.OnKeyCodePressed = util.BasicKeyHandler
-- contents wrapper
local dcont = vgui.Create("DPanel", dframe)
dcont:SetPaintBackground(false)
dcont:SetSize(rw, rh)
dcont:SetPos(m, 25 + m)
-- icon list
local dlist = vgui.Create("DPanelSelect", dcont)
dlist:SetPos(listx, listy)
dlist:SetSize(listw, listh)
dlist:EnableHorizontal(true)
dlist:SetSpacing(1)
dlist:SetPadding(2)
if dlist.VBar then
dlist.VBar:Remove()
dlist.VBar = nil
end
-- description area
local dscroll = vgui.Create("DHorizontalScroller", dlist)
dscroll:StretchToParent(3,3,3,3)
local ddesc = vgui.Create("ColoredBox", dcont)
ddesc:SetColor(Color(50, 50, 50))
ddesc:SetName(T("search_info"))
ddesc:SetPos(descx, descy)
ddesc:SetSize(descw, desch)
local dactive = vgui.Create("DImage", ddesc)
dactive:SetImage("vgui/ttt/icon_id")
dactive:SetPos(m, m)
dactive:SetSize(64, 64)
local dtext = vgui.Create("ScrollLabel", ddesc)
dtext:SetSize(descw - 120, desch - m*2)
dtext:MoveRightOf(dactive, m*2)
dtext:AlignTop(m)
dtext:SetText("...")
-- buttons
local by = rh - bh - (m/2)
local dident = vgui.Create("DButton", dcont)
dident:SetPos(m, by)
dident:SetSize(bw_large, bh)
dident:SetText(T("search_confirm"))
local id = search_raw.eidx + search_raw.dtime
dident.DoClick = function() RunConsoleCommand("ttt_confirm_death", search_raw.eidx, id) end
dident:SetDisabled(client:IsSpec() or (not client:KeyDownLast(IN_WALK)))
local dcall = vgui.Create("DButton", dcont)
dcall:SetPos(m*2 + bw_large, by)
dcall:SetSize(bw_large, bh)
dcall:SetText(T("search_call"))
dcall.DoClick = function(s)
client.called_corpses = client.called_corpses or {}
table.insert(client.called_corpses, search_raw.eidx)
s:SetDisabled(true)
RunConsoleCommand("ttt_call_detective", search_raw.eidx)
end
dcall:SetDisabled(client:IsSpec() or table.HasValue(client.called_corpses or {}, search_raw.eidx))
local dconfirm = vgui.Create("DButton", dcont)
dconfirm:SetPos(rw - m - bw, by)
dconfirm:SetSize(bw, bh)
dconfirm:SetText(T("close"))
dconfirm.DoClick = function() dframe:Close() end
-- Finalize search data, prune stuff that won't be shown etc
-- search is a table of tables that have an img and text key
local search = PreprocSearch(search_raw)
-- Install info controller that will link up the icons to the text etc
dlist.OnActivePanelChanged = SearchInfoController(search, dactive, dtext)
-- Create table of SimpleIcons, each standing for a piece of search
-- information.
local start_icon = nil
for t, info in SortedPairsByMemberValue(search, "p") do
local ic = nil
-- Certain items need a special icon conveying additional information
if t == "nick" then
local avply = IsValid(search_raw.owner) and search_raw.owner or nil
ic = vgui.Create("SimpleIconAvatar", dlist)
ic:SetPlayer(avply)
start_icon = ic
elseif t == "lastid" then
ic = vgui.Create("SimpleIconAvatar", dlist)
ic:SetPlayer(info.ply)
ic:SetAvatarSize(24)
elseif info.text_icon then
ic = vgui.Create("SimpleIconLabelled", dlist)
ic:SetIconText(info.text_icon)
else
ic = vgui.Create("SimpleIcon", dlist)
end
ic:SetIconSize(64)
ic:SetIcon(info.img)
ic.info_type = t
dlist:AddPanel(ic)
dscroll:AddPanel(ic)
end
dlist:SelectPanel(start_icon)
dframe:MakePopup()
end
local function StoreSearchResult(search)
if search.owner then
-- if existing result was not ours, it was detective's, and should not
-- be overwritten
local ply = search.owner
if (not ply.search_result) or ply.search_result.show then
ply.search_result = search
-- this is useful for targetid
local rag = Entity(search.eidx)
if IsValid(rag) then
rag.search_result = search
end
end
end
end
local function bitsRequired(num)
local bits, max = 0, 1
while max <= num do
bits = bits + 1
max = max + max
end
return bits
end
local plyBits = bitsRequired(game.MaxPlayers())
local search = {}
local function ReceiveRagdollSearch()
search = {}
-- Basic info
search.eidx = net.ReadUInt(16)
search.owner = Entity(net.ReadUInt(plyBits))
if not (IsValid(search.owner) and search.owner:IsPlayer() and (not search.owner:IsTerror())) then
search.owner = nil
end
search.nick = net.ReadString()
-- Equipment
local eq = net.ReadUInt(bitsRequired(EQUIP_MAX))
-- All equipment pieces get their own icon
search.eq_armor = util.BitSet(eq, EQUIP_ARMOR)
search.eq_radar = util.BitSet(eq, EQUIP_RADAR)
search.eq_disg = util.BitSet(eq, EQUIP_DISGUISE)
-- Traitor things
search.role = net.ReadUInt(2)
search.c4 = net.ReadUInt(bitsRequired(C4_WIRE_COUNT))
-- Kill info
search.dmg = net.ReadUInt(30)
search.wep = net.ReadString()
search.head = net.ReadBool()
search.dtime = net.ReadUInt(15)
search.stime = net.ReadUInt(15)
-- Players killed
local num_kills = net.ReadUInt(8)
if num_kills > 0 then
search.kills = {}
for i=1,num_kills do
table.insert(search.kills, net.ReadUInt(plyBits))
end
else
search.kills = nil
end
search.lastid = {idx=net.ReadUInt(plyBits)}
-- should we show a menu for this result?
search.finder = net.ReadUInt(plyBits)
search.show = (LocalPlayer():EntIndex() == search.finder)
--
-- last words
--
local words = net.ReadString()
search.words = (words ~= "") and words or nil
hook.Call("TTTBodySearchEquipment", nil, search, eq)
if search.show and hook.Run("TTTShowSearchScreen", search) ~= false then
ShowSearchScreen(search)
end
StoreSearchResult(search)
search = nil
end
net.Receive("TTT_RagdollSearch", ReceiveRagdollSearch)