add sborka

This commit is contained in:
2026-03-31 10:27:04 +03:00
commit f5e5f56c84
2345 changed files with 382127 additions and 0 deletions

View File

@@ -0,0 +1,603 @@
include("shared.lua")
ENT.BoardRT = nil
ENT.BoardMat = nil
ENT.Elements = {}
ENT.ImageCache = {}
function ENT:Initialize()
-- Создаем уникальный холст (RenderTarget) 1024x512 для рисования маркером
self.BoardRT = GetRenderTarget("SchoolBoardRT_" .. self:EntIndex(), 1024, 512)
self.BoardMat = CreateMaterial("SchoolBoardMat_" .. self:EntIndex(), "UnlitGeneric", {
["$basetexture"] = self.BoardRT:GetName(),
["$translucent"] = 1
})
self.Elements = {}
self.ImageCache = {}
self.PositionOffset = Vector(3713.2, 4595.6, -200) -- Инициализируем смещение позиции
self.RotationOffset = Angle(0, -180, 90) -- Инициализируем смещение поворота
-- Заливаем доску цветом белый
render.PushRenderTarget(self.BoardRT)
render.Clear(255, 255, 255, 255)
render.PopRenderTarget()
-- Запрашиваем актуальный макет элементов (картинки, текст)
net.Start("SchoolBoard_RequestLayout")
net.WriteEntity(self)
net.SendToServer()
end
local function GetBoardImage(ent, url)
if not url or url == "" then return nil end
if ent.ImageCache[url] then return ent.ImageCache[url] end
ent.ImageCache[url] = Material("error") -- плейсхолдер пока грузится
local fetchUrl = url
if string.match(fetchUrl, "^https?://imgur%.com/([^/]+)$") then
local id = string.match(fetchUrl, "^https?://imgur%.com/([^/]+)$")
fetchUrl = "https://i.imgur.com/" .. id .. ".png"
end
http.Fetch(fetchUrl, function(body, length, headers, code)
if code == 200 then
local contentType = headers["Content-Type"] or headers["content-type"] or ""
if not string.find(string.lower(contentType), "image") then
chat.AddText(Color(255, 50, 50), "[Доска] Ссылка не является картинкой: ", url)
return
end
local filename = "schoolboard_img_" .. util.CRC(url) .. ".png"
file.Write(filename, body)
ent.ImageCache[url] = Material("data/" .. filename, "noclamp smooth")
end
end)
return ent.ImageCache[url]
end
net.Receive("SchoolBoard_SyncLayout", function()
local ent = net.ReadEntity()
local size = net.ReadUInt(32)
local compressed = net.ReadData(size)
if IsValid(ent) and ent:GetClass() == "school_board" then
local json = util.Decompress(compressed) or "[]"
ent.Elements = util.JSONToTable(json) or {}
end
end)
net.Receive("SchoolBoard_SyncPosition", function()
local ent = net.ReadEntity()
local offsetX = net.ReadFloat()
local offsetY = net.ReadFloat()
local offsetZ = net.ReadFloat()
if IsValid(ent) and ent:GetClass() == "school_board" then
ent.PositionOffset = Vector(offsetX, offsetY, offsetZ)
end
end)
net.Receive("SchoolBoard_SyncRotation", function()
local ent = net.ReadEntity()
local pitch = net.ReadFloat()
local yaw = net.ReadFloat()
local roll = net.ReadFloat()
if IsValid(ent) and ent:GetClass() == "school_board" then
ent.RotationOffset = Angle(pitch, yaw, roll)
end
end)
function ENT:GetBoardMetrics()
-- Фиксированный масштаб, который гарантированно влезает на доску
local scale = 0.088
local width = 93
local height = 47
local realW = 1024 * scale
local realH = 512 * scale
-- Центрируем холст 1024x512 относительно ширины 93 и высоты 47
local offsetY = (width - realW) / 2
local offsetZ = (height - realH) / 2
-- Исходные крайние точки из модели: Y = -46.5, Z = 23.5
local startY = -46.5 + offsetY
local startZ = 23.5 - offsetZ
local frontX = 1.25 -- Чуть дальше 1.2, чтобы избежать z-fighting
-- Смещение позиции применяем в локальных координатах пропа
local posOffset = self.PositionOffset or Vector(0, 0, 0)
local rotOffset = self.RotationOffset or Angle(0, 0, 270)
-- Базовая позиция + смещение в локальных координатах пропа
local localPos = Vector(frontX + posOffset.x * 0.01, startY + posOffset.y * 0.01, startZ + posOffset.z * 0.01)
local pos = self:LocalToWorld(localPos)
local ang = self:LocalToWorldAngles(rotOffset)
return pos, ang, scale, startY, startZ, frontX
end
function ENT:Draw()
self:DrawModel()
local pos, ang, scale, startY, startZ, frontX = self:GetBoardMetrics()
self.BoardStartY = startY
self.BoardStartZ = startZ
self.BoardScale = scale
self.BoardFrontX = frontX
cam.Start3D2D(pos, ang, scale)
-- Отрисовка слоя с маркерами (RenderTarget)
surface.SetMaterial(self.BoardMat)
surface.SetDrawColor(255, 255, 255, 255)
surface.DrawTexturedRect(0, 0, 1024, 512)
-- Отрисовка картинок и текста (native, четко)
for _, el in ipairs(self.Elements or {}) do
if el.type == "image" then
local mat = GetBoardImage(self, el.url)
if mat and mat:GetName() ~= "error" then
surface.SetMaterial(mat)
surface.SetDrawColor(255, 255, 255, 255)
surface.DrawTexturedRect(el.x, el.y, el.w, el.h)
end
elseif el.type == "text" then
draw.DrawText(el.text, "DermaLarge", el.x, el.y, el.color or color_white, TEXT_ALIGN_LEFT)
end
end
cam.End3D2D()
end
-- ==========================================
-- MENU / EDITOR
-- ==========================================
net.Receive("SchoolBoard_OpenMenu", function()
local ent = net.ReadEntity()
if not IsValid(ent) then return end
local local_elements = table.Copy(ent.Elements or {})
local selected_idx = nil
local frame = vgui.Create("DFrame")
-- Делаем окно адаптивным под разрешение игрока
local fw, fh = ScrW() * 0.9, ScrH() * 0.9
frame:SetSize(fw, fh)
frame:Center()
frame:SetTitle("Редактор Интерактивной Доски")
frame:MakePopup()
-- ЛЕВАЯ ПАНЕЛЬ: Инструменты
local leftPanel = vgui.Create("DPanel", frame)
leftPanel:Dock(LEFT)
leftPanel:SetWide(math.max(200, fw * 0.15))
leftPanel:DockMargin(5, 5, 5, 5)
-- ПРАВАЯ ПАНЕЛЬ: Свойства выбранного элемента
local rightPanel = vgui.Create("DPanel", frame)
rightPanel:Dock(RIGHT)
rightPanel:SetWide(math.max(250, fw * 0.2))
rightPanel:DockMargin(5, 5, 5, 5)
-- ЦЕНТР: Холст
local centerPanel = vgui.Create("DPanel", frame)
centerPanel:Dock(FILL)
centerPanel:DockMargin(5, 5, 5, 5)
centerPanel.Paint = function(self, w, h)
surface.SetDrawColor(50, 50, 50, 255)
surface.DrawRect(0, 0, w, h)
end
local titleHint = vgui.Create("DLabel", centerPanel)
titleHint:Dock(TOP)
titleHint:DockMargin(10, 10, 10, 0)
titleHint:SetFont("DermaDefaultBold")
titleHint:SetText("Холст 1024x512. Перетаскивайте элементы мышью.")
titleHint:SetTextColor(color_white)
titleHint:SetContentAlignment(5)
local canvasCanvas = vgui.Create("DPanel", centerPanel)
canvasCanvas:Dock(FILL)
canvasCanvas:DockMargin(20, 10, 20, 20)
canvasCanvas.Paint = function() end -- transparent wrapper
-- Сам холст
local canvasPanel = vgui.Create("DPanel", canvasCanvas)
canvasPanel:SetMouseInputEnabled(true)
-- Вычисляем размеры холста, чтобы сохранить пропорции 2:1
canvasCanvas.PerformLayout = function(self, w, h)
local ratio = 1024 / 512
local cw, ch = w, w / ratio
if ch > h then
ch = h
cw = h * ratio
end
canvasPanel:SetSize(cw, ch)
canvasPanel:SetPos((w - cw) / 2, (h - ch) / 2)
end
-- Отрисовка холста (с маппингом из 1024x512 в текущее разрешение cw x ch)
canvasPanel.Paint = function(self, w, h)
local scaleX = w / 1024
local scaleY = h / 512
surface.SetDrawColor(255, 255, 255, 255)
surface.DrawRect(0, 0, w, h)
for i, el in ipairs(local_elements) do
local drawX = el.x * scaleX
local drawY = el.y * scaleY
local drawW = (el.w or 100) * scaleX
local drawH = (el.h or 30) * scaleY
if el.type == "image" then
local mat = GetBoardImage(ent, el.url)
if mat and mat:GetName() ~= "error" then
surface.SetMaterial(mat)
surface.SetDrawColor(255, 255, 255, 255)
surface.DrawTexturedRect(drawX, drawY, drawW, drawH)
else
surface.SetDrawColor(200, 200, 200, 255)
surface.DrawRect(drawX, drawY, drawW, drawH)
draw.SimpleText("Image", "DermaDefault", drawX + drawW/2, drawY + drawH/2, color_black, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
end
elseif el.type == "text" then
local m = Matrix()
m:Translate(Vector(drawX, drawY, 0))
m:Scale(Vector(scaleX, scaleY, 1))
cam.PushModelMatrix(m)
draw.DrawText(el.text, "DermaLarge", 0, 0, el.color or color_white, TEXT_ALIGN_LEFT)
cam.PopModelMatrix()
end
if i == selected_idx then
surface.SetDrawColor(255, 0, 0, 255)
surface.DrawOutlinedRect(drawX, drawY, drawW, drawH, 2)
end
end
end
local dragging = false
local dragOffset = {x=0,y=0}
canvasPanel.OnMousePressed = function(self, key)
if key == MOUSE_LEFT then
local mx, my = self:CursorPos()
local w, h = self:GetSize()
local scaleX = 1024 / w
local scaleY = 512 / h
local realX = mx * scaleX
local realY = my * scaleY
selected_idx = nil
for i = #local_elements, 1, -1 do
local el = local_elements[i]
local ew = el.w or 100
local eh = el.h or 30
if el.type == "text" then
surface.SetFont("DermaLarge")
ew, eh = surface.GetTextSize(el.text)
el.w = ew
el.h = eh
end
if realX >= el.x and realX <= el.x + ew and realY >= el.y and realY <= el.y + eh then
selected_idx = i
dragging = true
dragOffset.x = realX - el.x
dragOffset.y = realY - el.y
break
end
end
UpdatePropertiesPanel()
end
end
canvasPanel.OnMouseReleased = function(self, key)
if key == MOUSE_LEFT then dragging = false end
end
canvasPanel.Think = function(self)
if dragging and selected_idx and local_elements[selected_idx] then
local mx, my = self:CursorPos()
local w, h = self:GetSize()
local scaleX = 1024 / w
local scaleY = 512 / h
local realX = mx * scaleX
local realY = my * scaleY
local_elements[selected_idx].x = realX - dragOffset.x
local_elements[selected_idx].y = realY - dragOffset.y
end
end
local toolsPanel = vgui.Create("DScrollPanel", leftPanel)
toolsPanel:Dock(FILL)
local lblTools = toolsPanel:Add("DLabel")
lblTools:Dock(TOP)
lblTools:DockMargin(5,5,5,10)
lblTools:SetText("Инструменты")
lblTools:SetFont("DermaDefaultBold")
lblTools:SetDark(true)
local btnAddImg = toolsPanel:Add("DButton")
btnAddImg:Dock(TOP)
btnAddImg:DockMargin(5,0,5,5)
btnAddImg:SetText("Добавить Картинку")
btnAddImg.DoClick = function()
Derma_StringRequest("URL Картинки", "Введите прямую ссылку (.png/.jpg):", "", function(text)
table.insert(local_elements, {type="image", url=text, x=512-150, y=256-150, w=300, h=300})
selected_idx = #local_elements
UpdatePropertiesPanel()
end)
end
local btnAddText = toolsPanel:Add("DButton")
btnAddText:Dock(TOP)
btnAddText:DockMargin(5,0,5,15)
btnAddText:SetText("Добавить Текст")
btnAddText.DoClick = function()
table.insert(local_elements, {type="text", text="Новый текст", x=512-50, y=256-15, color=Color(255,255,255)})
selected_idx = #local_elements
UpdatePropertiesPanel()
end
local lblControl = toolsPanel:Add("DLabel")
lblControl:Dock(TOP)
lblControl:DockMargin(5,10,5,5)
lblControl:SetText("Управление доской")
lblControl:SetFont("DermaDefaultBold")
lblControl:SetDark(true)
local btnSave = toolsPanel:Add("DButton")
btnSave:Dock(TOP)
btnSave:DockMargin(5,0,5,5)
btnSave:SetText("Сохранить на Доску")
btnSave:SetTall(35)
btnSave.DoClick = function()
-- Сохраняем элементы (текст и картинки)
local json = util.TableToJSON(local_elements)
local compressed = util.Compress(json)
net.Start("SchoolBoard_SaveLayout")
net.WriteEntity(ent)
net.WriteUInt(string.len(compressed), 32)
net.WriteData(compressed, string.len(compressed))
net.SendToServer()
chat.AddText(Color(0,255,0), "[Доска] Успешно сохранено!")
frame:Close()
end
local btnClearDraw = toolsPanel:Add("DButton")
btnClearDraw:Dock(TOP)
btnClearDraw:DockMargin(5,5,5,15)
btnClearDraw:SetText("Стереть 3D-маркер")
btnClearDraw.DoClick = function()
net.Start("SchoolBoard_Clear_Req")
net.WriteEntity(ent)
net.SendToServer()
end
local chalkLabel = toolsPanel:Add("DLabel")
chalkLabel:Dock(TOP)
chalkLabel:DockMargin(5,10,5,5)
chalkLabel:SetText("Цвет 3D-маркера:")
chalkLabel:SetDark(true)
local chalkMixer = toolsPanel:Add("DColorMixer")
chalkMixer:Dock(TOP)
chalkMixer:SetTall(100)
chalkMixer:SetPalette(true)
chalkMixer:SetAlphaBar(false)
chalkMixer:SetWangs(true)
chalkMixer:SetColor(LocalPlayer().BoardChalkColor or Color(255,255,255))
chalkMixer.ValueChanged = function(s, c)
LocalPlayer().BoardChalkColor = c
end
local propsScroll = vgui.Create("DScrollPanel", rightPanel)
propsScroll:Dock(FILL)
function UpdatePropertiesPanel()
propsScroll:Clear()
local lblProps = propsScroll:Add("DLabel")
lblProps:Dock(TOP)
lblProps:DockMargin(5,5,5,10)
lblProps:SetText("Свойства элемента")
lblProps:SetFont("DermaDefaultBold")
lblProps:SetDark(true)
if selected_idx and local_elements[selected_idx] then
local el = local_elements[selected_idx]
local btnDel = propsScroll:Add("DButton")
btnDel:Dock(TOP)
btnDel:DockMargin(5,0,5,10)
btnDel:SetText("Удалить элемент")
btnDel:SetTextColor(Color(200, 50, 50))
btnDel.DoClick = function()
table.remove(local_elements, selected_idx)
selected_idx = nil
UpdatePropertiesPanel()
end
if el.type == "image" then
local wSlider = propsScroll:Add("DNumSlider")
wSlider:Dock(TOP)
wSlider:DockMargin(5,0,5,0)
wSlider:SetText("Ширина")
wSlider:SetMinMax(10, 1024)
wSlider:SetDecimals(0)
wSlider:SetValue(el.w)
wSlider.OnValueChanged = function(s, val) el.w = val end
local hSlider = propsScroll:Add("DNumSlider")
hSlider:Dock(TOP)
hSlider:DockMargin(5,0,5,10)
hSlider:SetText("Высота")
hSlider:SetMinMax(10, 1024)
hSlider:SetDecimals(0)
hSlider:SetValue(el.h)
hSlider.OnValueChanged = function(s, val) el.h = val end
elseif el.type == "text" then
local tEntry = propsScroll:Add("DTextEntry")
tEntry:Dock(TOP)
tEntry:DockMargin(5,0,5,10)
tEntry:SetValue(el.text)
tEntry.OnChange = function(s) el.text = s:GetValue() end
local lblC = propsScroll:Add("DLabel")
lblC:Dock(TOP)
lblC:DockMargin(5,5,5,0)
lblC:SetText("Цвет текста:")
lblC:SetDark(true)
local colMixer = propsScroll:Add("DColorMixer")
colMixer:Dock(TOP)
colMixer:SetTall(120)
colMixer:SetAlphaBar(false)
colMixer:DockMargin(5,0,5,10)
colMixer:SetColor(el.color or color_white)
colMixer.ValueChanged = function(s, c) el.color = c end
end
local btnUp = propsScroll:Add("DButton")
btnUp:Dock(TOP)
btnUp:DockMargin(5,5,5,5)
btnUp:SetText("На передний план")
btnUp.DoClick = function()
local temp = table.remove(local_elements, selected_idx)
table.insert(local_elements, temp)
selected_idx = #local_elements
UpdatePropertiesPanel()
end
local btnDown = propsScroll:Add("DButton")
btnDown:Dock(TOP)
btnDown:DockMargin(5,0,5,5)
btnDown:SetText("На задний план")
btnDown.DoClick = function()
local temp = table.remove(local_elements, selected_idx)
table.insert(local_elements, 1, temp)
selected_idx = 1
UpdatePropertiesPanel()
end
else
local hint = propsScroll:Add("DLabel")
hint:Dock(TOP)
hint:DockMargin(5,0,5,0)
hint:SetText("Выберите элемент на холсте,\nчтобы изменить его свойства.")
hint:SetDark(true)
hint:SizeToContents()
end
end
UpdatePropertiesPanel()
end)
-- ==========================================
-- CHALK DRAWING LOGIC (MARKER)
-- ==========================================
net.Receive("SchoolBoard_Clear_Do", function()
local ent = net.ReadEntity()
if IsValid(ent) and ent.BoardRT then
render.PushRenderTarget(ent.BoardRT)
render.Clear(180, 140, 100, 255)
render.PopRenderTarget()
end
end)
net.Receive("SchoolBoard_DrawLine", function()
local ent = net.ReadEntity()
local x1 = net.ReadFloat()
local y1 = net.ReadFloat()
local x2 = net.ReadFloat()
local y2 = net.ReadFloat()
local r = net.ReadUInt(8)
local g = net.ReadUInt(8)
local b = net.ReadUInt(8)
if IsValid(ent) and ent.BoardRT then
render.PushRenderTarget(ent.BoardRT)
cam.Start2D()
surface.SetDrawColor(r, g, b, 255)
surface.DrawLine(x1, y1, x2, y2)
surface.DrawLine(x1 + 1, y1, x2 + 1, y2)
surface.DrawLine(x1, y1 + 1, x2, y2 + 1)
cam.End2D()
render.PopRenderTarget()
end
end)
local lastDrawX, lastDrawY = nil, nil
hook.Add("Think", "SchoolBoard_DrawLogic", function()
local ply = LocalPlayer()
if not input.IsMouseDown(MOUSE_LEFT) then
lastDrawX = nil
lastDrawY = nil
return
end
local tr = ply:GetEyeTrace()
local ent = tr.Entity
if IsValid(ent) and ent:GetClass() == "school_board" and ent.BoardRT then
if ply:GetPos():DistToSqr(ent:GetPos()) > 40000 then return end
if not ent.BoardScale then return end
local obbPos = ent:WorldToLocal(tr.HitPos)
-- Проверка: не даем рисовать с обратной стороны доски
if obbPos.x < ent.BoardFrontX - 2 then return end
local drawX = (obbPos.y - ent.BoardStartY) / ent.BoardScale
local drawY = (ent.BoardStartZ - obbPos.z) / ent.BoardScale
if drawX < 0 or drawX > 1024 or drawY < 0 or drawY > 512 then
lastDrawX = nil
lastDrawY = nil
return
end
local chalkColor = ply.BoardChalkColor or Color(255, 255, 255)
if lastDrawX and lastDrawY then
render.PushRenderTarget(ent.BoardRT)
cam.Start2D()
surface.SetDrawColor(chalkColor.r, chalkColor.g, chalkColor.b, 255)
surface.DrawLine(lastDrawX, lastDrawY, drawX, drawY)
surface.DrawLine(lastDrawX + 1, lastDrawY, drawX + 1, drawY)
surface.DrawLine(lastDrawX, lastDrawY + 1, drawX, drawY + 1)
cam.End2D()
render.PopRenderTarget()
net.Start("SchoolBoard_DrawLine", true)
net.WriteEntity(ent)
net.WriteFloat(lastDrawX)
net.WriteFloat(lastDrawY)
net.WriteFloat(drawX)
net.WriteFloat(drawY)
net.WriteUInt(chalkColor.r, 8)
net.WriteUInt(chalkColor.g, 8)
net.WriteUInt(chalkColor.b, 8)
net.SendToServer()
end
lastDrawX = drawX
lastDrawY = drawY
else
lastDrawX = nil
lastDrawY = nil
end
end)

View File

@@ -0,0 +1,274 @@
AddCSLuaFile("cl_init.lua")
AddCSLuaFile("shared.lua")
include("shared.lua")
-- Регистрация всех сетевых сообщений
util.AddNetworkString("SchoolBoard_OpenMenu")
util.AddNetworkString("SchoolBoard_SaveLayout")
util.AddNetworkString("SchoolBoard_SyncLayout")
util.AddNetworkString("SchoolBoard_RequestLayout")
util.AddNetworkString("SchoolBoard_Clear_Req")
util.AddNetworkString("SchoolBoard_Clear_Do")
util.AddNetworkString("SchoolBoard_DrawLine")
util.AddNetworkString("SchoolBoard_SavePosition")
util.AddNetworkString("SchoolBoard_SyncPosition")
util.AddNetworkString("SchoolBoard_SaveRotation")
util.AddNetworkString("SchoolBoard_SyncRotation")
-- helper: only SAM ranks superadmin/curator (or equivalent usergroups) can modify board
local function CanModifyBoard(ply)
if not IsValid(ply) or not ply:IsPlayer() then return false end
-- standard checks
if ply:IsSuperAdmin() then return true end
if ply:GetUserGroup() == "curator" then return true end
-- if SAM is present, consult its API as a fallback
if sam and sam.IsPlayerInRank then
if sam.IsPlayerInRank(ply, "superadmin") or sam.IsPlayerInRank(ply, "curator") then
return true
end
end
return false
end
-- append log entries to a file and console
local function LogBoardChange(ply, ent, action)
local name = IsValid(ply) and ply:Nick() or "Console"
local sid = IsValid(ply) and ply:SteamID() or "N/A"
local id = IsValid(ent) and ent:EntIndex() or 0
local pos = IsValid(ent) and tostring(ent:GetPos()) or "unknown"
local msg = string.format("[%s] %s (%s) %s board #%d at %s\n", os.date(), name, sid, action, id, pos)
print(msg)
file.Append("schoolboard_changes.txt", msg)
end
-- console command for printing current log (admins only)
concommand.Add("schoolboard_printlog", function(ply, cmd, args)
if IsValid(ply) and not CanModifyBoard(ply) then
ply:ChatPrint("Нет доступа к логу доски.")
return
end
local contents = file.Read("schoolboard_changes.txt", "DATA") or ""
if IsValid(ply) then
ply:PrintMessage(HUD_PRINTCONSOLE, contents)
ply:ChatPrint("Лог выведен в консоль.")
else
print(contents)
end
end)
-- очистка лога доски (только для старшего админства)
concommand.Add("schoolboard_clearlog", function(ply, cmd, args)
if IsValid(ply) and not CanModifyBoard(ply) then
ply:ChatPrint("Нет доступа к управлению логом.")
return
end
file.Write("schoolboard_changes.txt", "")
if IsValid(ply) then
ply:ChatPrint("Лог доски очищен.")
ply:PrintMessage(HUD_PRINTCONSOLE, "Лог доски был очищен.")
else
print("schoolboard_changes.txt was cleared by console")
end
end)
function ENT:Initialize()
self:SetModel("models/props/cs_office/offcorkboarda.mdl")
self:PhysicsInit(SOLID_VPHYSICS)
self:SetMoveType(MOVETYPE_VPHYSICS)
self:SetSolid(SOLID_VPHYSICS)
self:SetUseType(SIMPLE_USE) -- Позволяет нажимать 'E' на доску
local phys = self:GetPhysicsObject()
if IsValid(phys) then
phys:Wake()
end
self.BoardLayoutStr = "[]" -- По умолчанию пустой массив JSON
self.PositionOffset = Vector(3713.2, 4595.6, -200) -- Смещение позиции доски в 3D пространстве
self.RotationOffset = Angle(0, -180, 90) -- Смещение поворота доски
end
-- Открываем меню настройки, когда игрок нажимает 'E'
function ENT:Use(activator, caller)
if IsValid(activator) and activator:IsPlayer() then
if not CanModifyBoard(activator) then
activator:ChatPrint("Только superadmin/curator может редактировать доску.")
return
end
net.Start("SchoolBoard_OpenMenu")
net.WriteEntity(self)
net.Send(activator)
end
end
-- Клиент отправляет свой новый макет доски (массив картинок и текстов)
net.Receive("SchoolBoard_SaveLayout", function(len, ply)
local ent = net.ReadEntity()
if len > 64000 then -- Ограничение размера пакета на всякий случай
return
end
local layoutSize = net.ReadUInt(32)
local compressedData = net.ReadData(layoutSize)
if IsValid(ent) and ent:GetClass() == "school_board" then
-- Разрешены только superadmin/curator (SAM ранги) или соответствующие usergroup
if not CanModifyBoard(ply) then
if IsValid(ply) then ply:ChatPrint("У вас нет прав для изменения доски.") end
return
end
-- Проверка расстояния, чтобы школьник с другого конца карты не менял доску
if ply:GetPos():DistToSqr(ent:GetPos()) > 500000 then return end
ent.BoardLayoutStr = util.Decompress(compressedData) or "[]"
-- логируем факт сохранения
LogBoardChange(ply, ent, "saved layout")
-- Рассказываем всем остальным, что доска обновилась
net.Start("SchoolBoard_SyncLayout")
net.WriteEntity(ent)
net.WriteUInt(layoutSize, 32)
net.WriteData(compressedData, layoutSize)
net.Broadcast()
end
end)
-- Когда игрок подключается или загружает сущность, он просит её данные
net.Receive("SchoolBoard_RequestLayout", function(len, ply)
local ent = net.ReadEntity()
if IsValid(ent) and ent:GetClass() == "school_board" then
if not ent.BoardLayoutStr then ent.BoardLayoutStr = "[]" end
local compressedData = util.Compress(ent.BoardLayoutStr)
if compressedData then
local layoutSize = string.len(compressedData)
net.Start("SchoolBoard_SyncLayout")
net.WriteEntity(ent)
net.WriteUInt(layoutSize, 32)
net.WriteData(compressedData, layoutSize)
net.Send(ply)
end
end
end)
-- Принимаем запрос на очистку от игрока и рассылаем всем
net.Receive("SchoolBoard_Clear_Req", function(len, ply)
local ent = net.ReadEntity()
if IsValid(ent) and ent:GetClass() == "school_board" then
if not CanModifyBoard(ply) then
if IsValid(ply) then ply:ChatPrint("У вас нет прав для изменения доски.") end
return
end
LogBoardChange(ply, ent, "cleared board")
net.Start("SchoolBoard_Clear_Do")
net.WriteEntity(ent)
net.Broadcast()
end
end)
-- Принимаем координаты линии и цвет от рисующего и рассылаем остальным
net.Receive("SchoolBoard_DrawLine", function(len, ply)
local ent = net.ReadEntity()
local x1 = net.ReadFloat()
local y1 = net.ReadFloat()
local x2 = net.ReadFloat()
local y2 = net.ReadFloat()
local r = net.ReadUInt(8)
local g = net.ReadUInt(8)
local b = net.ReadUInt(8)
if IsValid(ent) and ent:GetClass() == "school_board" then
-- ограничения на рисование маркером
if not CanModifyBoard(ply) then return end
-- Проверяем, что игрок стоит рядом с доской
if ply:GetPos():DistToSqr(ent:GetPos()) > 50000 then return end
net.Start("SchoolBoard_DrawLine", true) -- unreliable пакет
net.WriteEntity(ent)
net.WriteFloat(x1)
net.WriteFloat(y1)
net.WriteFloat(x2)
net.WriteFloat(y2)
net.WriteUInt(r, 8)
net.WriteUInt(g, 8)
net.WriteUInt(b, 8)
net.SendOmit(ply) -- Отправляем всем, кроме рисующего
end
end)
-- Сохраняем позицию доски (смещение вектора)
net.Receive("SchoolBoard_SavePosition", function(len, ply)
local ent = net.ReadEntity()
local offsetX = net.ReadFloat()
local offsetY = net.ReadFloat()
local offsetZ = net.ReadFloat()
if IsValid(ent) and ent:GetClass() == "school_board" then
if not CanModifyBoard(ply) then
if IsValid(ply) then ply:ChatPrint("У вас нет прав для изменения доски.") end
return
end
-- Проверка расстояния
if ply:GetPos():DistToSqr(ent:GetPos()) > 500000 then return end
ent.PositionOffset = Vector(offsetX, offsetY, offsetZ)
-- логируем изменение позиции
LogBoardChange(ply, ent, "changed position")
-- Синхронизируем позицию со всеми
net.Start("SchoolBoard_SyncPosition")
net.WriteEntity(ent)
net.WriteFloat(offsetX)
net.WriteFloat(offsetY)
net.WriteFloat(offsetZ)
net.Broadcast()
end
end)
-- Сохраняем поворот доски (смещение угла)
net.Receive("SchoolBoard_SaveRotation", function(len, ply)
local ent = net.ReadEntity()
local pitch = net.ReadFloat()
local yaw = net.ReadFloat()
local roll = net.ReadFloat()
if IsValid(ent) and ent:GetClass() == "school_board" then
if not CanModifyBoard(ply) then
if IsValid(ply) then ply:ChatPrint("У вас нет прав для изменения доски.") end
return
end
-- Проверка расстояния
if ply:GetPos():DistToSqr(ent:GetPos()) > 500000 then return end
ent.RotationOffset = Angle(pitch, yaw, roll)
-- логируем изменение поворота
LogBoardChange(ply, ent, "changed rotation")
-- Синхронизируем поворот со всеми
net.Start("SchoolBoard_SyncRotation")
net.WriteEntity(ent)
net.WriteFloat(pitch)
net.WriteFloat(yaw)
net.WriteFloat(roll)
net.Broadcast()
end
end)

View File

@@ -0,0 +1,11 @@
ENT.Type = "anim"
ENT.Base = "base_gmodentity"
ENT.PrintName = "Интерактивная доска"
ENT.Category = "Разное"
ENT.Author = "Welding"
ENT.Spawnable = true
function ENT:SetupDataTables()
-- Больше не используем NetworkVar для одной картинки.
-- Доска будет хранить массив элементов через JSON.
end