mirror of
https://github.com/lifestorm/wnsrc.git
synced 2025-12-16 21:33:46 +03:00
515 lines
14 KiB
Lua
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)
|