add sborka
This commit is contained in:
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user