--by pavetr.top/ds local SlitherGame = {} -- Кэширование функций local sin, cos, atan2, floor, ceil, sqrt, max, min, random, abs, huge = math.sin, math.cos, math.atan2, math.floor, math.ceil, math.sqrt, math.max, math.min, math.random, math.abs, math.huge local CurTime = CurTime local Lerp = Lerp -------------------------------- -- Параметры игры и базовые переменные -------------------------------- SlitherGame.boardWidth = 10000 SlitherGame.boardHeight = 10000 SlitherGame.targetPelletCount = 15000 -- количество гранул SlitherGame.aiSnakeCount = 30 -- число ИИ‑змей SlitherGame.basePlayerSpeed = 100 SlitherGame.baseAISpeed = 80 -- уменьшенная базовая скорость для ИИ SlitherGame.safeMargin = 50 -- граница, за которую змей погибает SlitherGame.spawnMargin = 250 -- отступ для спауна SlitherGame.exclusionMargin = 100 -- дополнительный отступ от области видимости игрока SlitherGame.cellSize = 200 -- ячейки для гранул SlitherGame.pelletCount = 0 SlitherGame.pelletGrid = {} SlitherGame.snakes = {} SlitherGame.playerSnake = nil SlitherGame.updateInterval = 0.02 SlitherGame.lastUpdateTime = CurTime() SlitherGame.gameActive = false SlitherGame.slitherActive = false SlitherGame.lastPlayerPos = nil -- позиция игрока для камеры после смерти -- Параметры LOD (уровень детализации) SlitherGame.LOD_THRESHOLD = 2000 -- расстояние, после которого применяется упрощённое обновление SlitherGame.LOD_UPDATE_INTERVAL = 0.1 -- минимальный интервал для упрощённого обновления объекта вне экрана SlitherGame.LOD_COLLISION_THRESHOLD = 3000 -- объекты дальше этого расстояния не участвуют в коллизиях (если не игрок) -------------------------------- -- Кэш для отрисовки кругов -------------------------------- SlitherGame.circleCache = {} local function drawCircle(x, y, radius, seg) seg = seg or 32 local key = radius .. "_" .. seg local poly = SlitherGame.circleCache[key] if not poly then poly = {} for i = 0, seg - 1 do local a = (i / seg) * 2 * math.pi poly[i + 1] = { x = cos(a) * radius, y = sin(a) * radius } end SlitherGame.circleCache[key] = poly end local polyDraw = {} for i, point in ipairs(poly) do polyDraw[i] = { x = x + point.x, y = y + point.y } end surface.DrawPoly(polyDraw) end -------------------------------- -- Вспомогательные функции -------------------------------- local function clamp(val, mn, mx) if val < mn then return mn end if val > mx then return mx end return val end local function angleDifference(a, b) local diff = (b - a) % (2 * math.pi) if diff > math.pi then diff = diff - 2 * math.pi end return diff end local function distanceSq(x1, y1, x2, y2) local dx = x1 - x2 local dy = y1 - y2 return dx * dx + dy * dy end local function isVisible(x, y, viewX, viewY, scrW, scrH) local margin = 50 return x >= viewX - scrW/2 - margin and x <= viewX + scrW/2 + margin and y >= viewY - scrH/2 - margin and y <= viewY + scrH/2 + margin end local function isSnakeVisible(snake, viewX, viewY, scrW, scrH) for _, seg in ipairs(snake.segments) do if isVisible(seg.x, seg.y, viewX, viewY, scrW, scrH) then return true end end return false end -------------------------------- -- Функции работы с гранулами -------------------------------- local function addPelletToGrid(pellet) local cellX = floor(pellet.x / SlitherGame.cellSize) + 1 local cellY = floor(pellet.y / SlitherGame.cellSize) + 1 pellet.cellX = cellX pellet.cellY = cellY SlitherGame.pelletGrid[cellX] = SlitherGame.pelletGrid[cellX] or {} SlitherGame.pelletGrid[cellX][cellY] = SlitherGame.pelletGrid[cellX][cellY] or {} table.insert(SlitherGame.pelletGrid[cellX][cellY], pellet) SlitherGame.pelletCount = SlitherGame.pelletCount + 1 end local function removePelletFromGrid(pellet) local cellX, cellY = pellet.cellX, pellet.cellY if SlitherGame.pelletGrid[cellX] and SlitherGame.pelletGrid[cellX][cellY] then local cell = SlitherGame.pelletGrid[cellX][cellY] for i = #cell, 1, -1 do if cell[i] == pellet then table.remove(cell, i) SlitherGame.pelletCount = SlitherGame.pelletCount - 1 return end end end end local function createPellet(x, y, col, size, glow, temporary) local pellet = { x = x or random(SlitherGame.safeMargin, SlitherGame.boardWidth - SlitherGame.safeMargin), y = y or random(SlitherGame.safeMargin, SlitherGame.boardHeight - SlitherGame.safeMargin), color = col or Color(random(50, 255), random(50, 255), random(50, 255)), size = size or random(3, 7), glow = glow or false, temporary = temporary or false, spawnTime = CurTime() } addPelletToGrid(pellet) return pellet end local function findClosestPellet(x, y) local bestPellet, bestDistSq = nil, huge for cx, col in pairs(SlitherGame.pelletGrid) do for cy, cell in pairs(col) do for _, pellet in ipairs(cell) do local dSq = (pellet.x - x)^2 + (pellet.y - y)^2 if dSq < bestDistSq then bestDistSq = dSq bestPellet = pellet end end end end if bestPellet then return { x = bestPellet.x, y = bestPellet.y } else return nil end end local function spawnDeathPellets(snake) for _, seg in ipairs(snake.segments) do createPellet(seg.x, seg.y, snake.color, random(8, 12), true, true) end end -------------------------------- -- Функции для оценки безопасности пути к грануле -------------------------------- local function pointToSegmentDistance(px, py, ax, ay, bx, by) local dx = bx - ax local dy = by - ay if dx == 0 and dy == 0 then return sqrt((px - ax)^2 + (py - ay)^2) end local t = ((px - ax) * dx + (py - ay) * dy) / (dx * dx + dy * dy) t = clamp(t, 0, 1) local projX = ax + t * dx local projY = ay + t * dy return sqrt((px - projX)^2 + (py - projY)^2) end local function isPathClear(snake, x1, y1, x2, y2, clearance) for _, other in ipairs(SlitherGame.snakes) do if other ~= snake and other.alive then for _, seg in ipairs(other.segments) do local d = pointToSegmentDistance(seg.x, seg.y, x1, y1, x2, y2) if d < clearance then return false end end end end return true end local function isPelletSafe(snake, pelletTarget, safetyMargin) for _, other in ipairs(SlitherGame.snakes) do if other ~= snake and other.alive then for _, seg in ipairs(other.segments) do local d = sqrt((seg.x - pelletTarget.x)^2 + (seg.y - pelletTarget.y)^2) if d < safetyMargin then return false end end end end return true end -------------------------------- -- Функции для засасывания гранул (как в slither.io) -------------------------------- local function updatePelletGridForPellet(pellet) local newCellX = floor(pellet.x / SlitherGame.cellSize) + 1 local newCellY = floor(pellet.y / SlitherGame.cellSize) + 1 if newCellX ~= pellet.cellX or newCellY ~= pellet.cellY then if SlitherGame.pelletGrid[pellet.cellX] and SlitherGame.pelletGrid[pellet.cellX][pellet.cellY] then local cell = SlitherGame.pelletGrid[pellet.cellX][pellet.cellY] for i = #cell, 1, -1 do if cell[i] == pellet then table.remove(cell, i) break end end end pellet.cellX = newCellX pellet.cellY = newCellY SlitherGame.pelletGrid[newCellX] = SlitherGame.pelletGrid[newCellX] or {} SlitherGame.pelletGrid[newCellX][newCellY] = SlitherGame.pelletGrid[newCellX][newCellY] or {} table.insert(SlitherGame.pelletGrid[newCellX][newCellY], pellet) end end local function attractPellets(snake) local head = snake.segments[1] local attractionRadius = 150 -- радиус действия эффекта local attractionStrength = 5 -- величина сдвига за обновление local angleThreshold = math.rad(30) -- ±30° от направления local forwardX = cos(snake.direction) local forwardY = sin(snake.direction) local headCellX = floor(head.x / SlitherGame.cellSize) + 1 local headCellY = floor(head.y / SlitherGame.cellSize) + 1 for cx = headCellX - 2, headCellX + 2 do if SlitherGame.pelletGrid[cx] then for cy = headCellY - 2, headCellY + 2 do if SlitherGame.pelletGrid[cx][cy] then for _, pellet in ipairs(SlitherGame.pelletGrid[cx][cy]) do local vx = pellet.x - head.x local vy = pellet.y - head.y local distSq = vx * vx + vy * vy if distSq < attractionRadius * attractionRadius then local dist = sqrt(distSq) if dist > 0 then local dot = (vx * forwardX + vy * forwardY) if dot > 0 then local cosAngle = dot / dist if cosAngle >= cos(angleThreshold) then local moveFactor = attractionStrength * (1 - dist / attractionRadius) pellet.x = pellet.x + (head.x - pellet.x) / dist * moveFactor pellet.y = pellet.y + (head.y - pellet.y) / dist * moveFactor updatePelletGridForPellet(pellet) end end end end end end end end end end -------------------------------- -- Создание змейки и вспомогательные функции -------------------------------- local function generateRandomName() local names = {"Alpha", "Bravo", "Charlie", "Delta", "Echo", "Foxtrot", "Golf", "Hotel", "India", "Juliet", "Kilo", "Lima", "Mike", "November", "Oscar", "Papa", "Quebec", "Romeo", "Sierra", "Tango", "Uniform", "Victor", "Whiskey", "Xray", "Yankee", "Zulu"} return names[random(#names)] .. "_" .. random(100,999) end local function getAISpawnPositionEvenly() if SlitherGame.playerSnake and SlitherGame.playerSnake.segments[1] then local scrW, scrH = ScrW(), ScrH() local viewX, viewY = SlitherGame.playerSnake.segments[1].x, SlitherGame.playerSnake.segments[1].y local left = viewX - scrW/2 - SlitherGame.exclusionMargin local right = viewX + scrW/2 + SlitherGame.exclusionMargin local top = viewY - scrH/2 - SlitherGame.exclusionMargin local bottom = viewY + scrH/2 + SlitherGame.exclusionMargin local maxAttempts = 100 for i = 1, maxAttempts do local x = random(SlitherGame.spawnMargin, SlitherGame.boardWidth - SlitherGame.spawnMargin) local y = random(SlitherGame.spawnMargin, SlitherGame.boardHeight - SlitherGame.spawnMargin) if not (x >= left and x <= right and y >= top and y <= bottom) then return { x = x, y = y } end end return { x = random(SlitherGame.spawnMargin, SlitherGame.boardWidth - SlitherGame.spawnMargin), y = random(SlitherGame.spawnMargin, SlitherGame.boardHeight - SlitherGame.spawnMargin) } else return { x = random(SlitherGame.spawnMargin, SlitherGame.boardWidth - SlitherGame.spawnMargin), y = random(SlitherGame.spawnMargin, SlitherGame.boardHeight - SlitherGame.spawnMargin) } end end local function getSafePlayerSpawnPosition() local maxAttempts = 100 for i = 1, maxAttempts do local candidate = { x = random(SlitherGame.spawnMargin, SlitherGame.boardWidth - SlitherGame.spawnMargin), y = random(SlitherGame.spawnMargin, SlitherGame.boardHeight - SlitherGame.spawnMargin) } local safe = true -- Проверяем все живые змеи, кроме игрока, чтобы они не находились слишком близко for _, snake in ipairs(SlitherGame.snakes) do if snake.alive and not snake.isPlayer then for _, seg in ipairs(snake.segments) do if sqrt((seg.x - candidate.x)^2 + (seg.y - candidate.y)^2) < 300 then safe = false break end end end if not safe then break end end if safe then return candidate end end return { x = SlitherGame.boardWidth / 2, y = SlitherGame.boardHeight / 2 } end local function createSnake(isPlayer) local snake = { segments = {}, direction = random() * 2 * math.pi, speed = isPlayer and SlitherGame.basePlayerSpeed or SlitherGame.baseAISpeed, currentSpeed = 0, grow = 0, alive = true, isPlayer = isPlayer or false, respawnTime = 0, accelLossTimer = 0, accelerating = false, initialLength = 0, spawnTime = CurTime(), color = nil, name = "", thickness = 25, } local startX, startY if isPlayer then local spawnPos = getSafePlayerSpawnPosition() startX = spawnPos.x startY = spawnPos.y snake.direction = random() * 2 * math.pi snake.initialLength = 10 for i = 1, 10 do table.insert(snake.segments, { x = startX - (i - 1) * 10 * cos(snake.direction), y = startY - (i - 1) * 10 * sin(snake.direction) }) end snake.name = LocalPlayer():Nick() or "Player" snake.thickness = 25 snake.nameAlpha = 255 else local initLength = random(10, 500) local maxAttempts = 100 local candidateSegments, safeToSpawn = nil, false for attempt = 1, maxAttempts do local pos = getAISpawnPositionEvenly() local candDir = random() * 2 * math.pi local maxLenX = (candDir >= 0 and cos(candDir) >= 0) and ((SlitherGame.boardWidth - SlitherGame.safeMargin - pos.x) / 10) or ((pos.x - SlitherGame.safeMargin) / 10) if cos(candDir) < 0 then maxLenX = (pos.x - SlitherGame.safeMargin) / 10 end local maxLenY = (sin(candDir) >= 0) and ((SlitherGame.boardHeight - SlitherGame.safeMargin - pos.y) / 10) or ((pos.y - SlitherGame.safeMargin) / 10) if sin(candDir) < 0 then maxLenY = (pos.y - SlitherGame.safeMargin) / 10 end local maxLen = floor(min(maxLenX, maxLenY)) + 1 local finalLength = math.min(initLength, maxLen) candidateSegments = {} for i = 1, finalLength do candidateSegments[i] = { x = pos.x - (i - 1) * 10 * cos(candDir), y = pos.y - (i - 1) * 10 * sin(candDir) } end local scrW, scrH = ScrW(), ScrH() local viewX, viewY = SlitherGame.playerSnake and SlitherGame.playerSnake.segments[1].x or SlitherGame.boardWidth/2, SlitherGame.playerSnake and SlitherGame.playerSnake.segments[1].y or SlitherGame.boardHeight/2 safeToSpawn = true for _, seg in ipairs(candidateSegments) do if seg.x < SlitherGame.safeMargin or seg.x > SlitherGame.boardWidth - SlitherGame.safeMargin or seg.y < SlitherGame.safeMargin or seg.y > SlitherGame.boardHeight - SlitherGame.safeMargin then safeToSpawn = false break end if isVisible(seg.x, seg.y, viewX, viewY, scrW, scrH) then safeToSpawn = false break end end if safeToSpawn then snake.direction = candDir snake.segments = candidateSegments snake.initialLength = finalLength break end end if not (snake.segments and snake.segments[1]) then snake.direction = random() * 2 * math.pi snake.initialLength = 10 snake.segments = {} local centerX = SlitherGame.boardWidth / 2 local centerY = SlitherGame.boardHeight / 2 for i = 1, 10 do snake.segments[i] = { x = centerX - (i - 1) * 10 * cos(snake.direction), y = centerY - (i - 1) * 10 * sin(snake.direction) } end end snake.name = generateRandomName() snake.thickness = 25 + ((snake.initialLength - 10) / 20) for i = 1, 20 do local head = snake.segments[1] local newX = head.x + cos(snake.direction) * snake.speed * SlitherGame.updateInterval local newY = head.y + sin(snake.direction) * snake.speed * SlitherGame.updateInterval table.insert(snake.segments, 1, { x = newX, y = newY }) table.remove(snake.segments) end end snake.currentSpeed = snake.speed snake.color = Color(random(50,255), random(50,255), random(50,255)) return snake end -------------------------------- -- Расчёт замедления в зависимости от длины змейки -------------------------------- local function getSlowdownFactor(snake) local L = #snake.segments if L < 100 then return 1.0 elseif L < 500 then return 0.95 - ((L - 100) / 400) * 0.05 elseif L < 1000 then return 0.90 - ((L - 500) / 500) * 0.15 elseif L < 5000 then return 0.75 - ((L - 1000) / 4000) * 0.05 elseif L < 10000 then return 0.70 - ((L - 5000) / 5000) * 0.40 else return 0.30 end end -------------------------------- -- Логика ИИ: выбор цели для движения -------------------------------- local function updateAIStrategy(snake, delta) local head = snake.segments[1] local headX, headY = head.x, head.y local dangerRadius = 300 local escapeVecX, escapeVecY = 0, 0 local threatDetected = false for _, other in ipairs(SlitherGame.snakes) do if other ~= snake and other.alive and other.segments[1] then local otherHead = other.segments[1] local dx = headX - otherHead.x local dy = headY - otherHead.y local dist = sqrt(dx*dx + dy*dy) if (#other.segments > #snake.segments) and (dist < dangerRadius) then threatDetected = true local factor = (dangerRadius - dist) / dangerRadius escapeVecX = escapeVecX + dx * factor escapeVecY = escapeVecY + dy * factor end end end if threatDetected and (escapeVecX ~= 0 or escapeVecY ~= 0) then return atan2(escapeVecY, escapeVecX) end local bestAttackScore = -huge local attackTarget = nil for _, other in ipairs(SlitherGame.snakes) do if other ~= snake and other.alive and other.segments[1] then if #snake.segments >= 2 * #other.segments then local otherHead = other.segments[1] local otherDir = other.direction or (other.segments[2] and atan2(other.segments[2].y - otherHead.y, other.segments[2].x - otherHead.x) or 0) local interceptDist = 100 local predictedX = otherHead.x + cos(otherDir) * interceptDist local predictedY = otherHead.y + sin(otherDir) * interceptDist local perpX, perpY = -sin(otherDir), cos(otherDir) local blockingOffset = 30 local side = (((headX - otherHead.x) * perpX + (headY - otherHead.y) * perpY) > 0) and 1 or -1 local blockX = predictedX + perpX * blockingOffset * side local blockY = predictedY + perpY * blockingOffset * side local penalty = 0 for i = 2, #other.segments do local seg = other.segments[i] local d = sqrt(distanceSq(seg.x, seg.y, blockX, blockY)) if d < 20 then penalty = penalty + 3 * (20 - d) end end local dx = blockX - headX local dy = blockY - headY local dist = sqrt(dx*dx + dy*dy) local alignment = cos(angleDifference(snake.direction, atan2(dy, dx))) local score = -dist + alignment * 50 - penalty if score > bestAttackScore then bestAttackScore = score attackTarget = { x = blockX, y = blockY } end end end end if attackTarget and bestAttackScore > 0 then return atan2(attackTarget.y - headY, attackTarget.x - headX) end local pelletTarget = findClosestPellet(headX, headY) if pelletTarget then local safetyMargin = snake.thickness + 20 if isPelletSafe(snake, pelletTarget, safetyMargin) and isPathClear(snake, headX, headY, pelletTarget.x, pelletTarget.y, safetyMargin) then return atan2(pelletTarget.y - headY, pelletTarget.x - headX) else return snake.direction end end return snake.direction end -------------------------------- -- Обновление змейки (общая логика для игрока и ИИ) -- (с применением LOD для ИИ‑змей вне центра вида) -------------------------------- local function updateSnakeFull(snake, delta, viewX, viewY) if not snake.alive then return end local head = snake.segments[1] if not head then return end if not snake.isPlayer then local dx = head.x - viewX local dy = head.y - viewY if (dx*dx + dy*dy) > (SlitherGame.LOD_THRESHOLD * SlitherGame.LOD_THRESHOLD) then snake.lastSimplifiedUpdate = snake.lastSimplifiedUpdate or 0 local now = CurTime() if now - snake.lastSimplifiedUpdate < SlitherGame.LOD_UPDATE_INTERVAL then return end snake.lastSimplifiedUpdate = now local dt = SlitherGame.LOD_UPDATE_INTERVAL local rawNewX = head.x + cos(snake.direction) * snake.speed * dt local rawNewY = head.y + sin(snake.direction) * snake.speed * dt if rawNewX < SlitherGame.safeMargin or rawNewX > SlitherGame.boardWidth - SlitherGame.safeMargin or rawNewY < SlitherGame.safeMargin or rawNewY > SlitherGame.boardHeight - SlitherGame.safeMargin then snake.alive = false snake.respawnTime = CurTime() + random(1,5) return end table.insert(snake.segments, 1, { x = rawNewX, y = rawNewY }) table.remove(snake.segments) return end end local desiredAngle = snake.direction if snake.isPlayer then local moveX, moveY = 0, 0 if input.IsKeyDown(KEY_W) then moveY = moveY - 1 end if input.IsKeyDown(KEY_S) then moveY = moveY + 1 end if input.IsKeyDown(KEY_A) then moveX = moveX - 1 end if input.IsKeyDown(KEY_D) then moveX = moveX + 1 end if moveX ~= 0 or moveY ~= 0 then local targetAngle = atan2(moveY, moveX) local extra = max(0, (#snake.segments - snake.initialLength) - 100) local maxTurnRate = clamp(2 * delta * (1 - extra / 10000), 0.01, 2 * delta) local diff = clamp(angleDifference(snake.direction, targetAngle), -maxTurnRate, maxTurnRate) desiredAngle = snake.direction + diff end local accelerating = input.IsKeyDown(KEY_SPACE) or input.IsKeyDown(KEY_UP) if accelerating and (#snake.segments > snake.initialLength) then snake.currentSpeed = snake.speed * 2.0 * getSlowdownFactor(snake) snake.accelLossTimer = snake.accelLossTimer + delta snake.accelerating = true if snake.accelLossTimer >= 0.1 then snake.accelLossTimer = snake.accelLossTimer - 0.1 if #snake.segments > snake.initialLength then local tail = table.remove(snake.segments) if tail then createPellet(tail.x, tail.y, snake.color, random(3,6), true, true) end end end else snake.currentSpeed = snake.speed * getSlowdownFactor(snake) snake.accelerating = false end else local targetAngle = updateAIStrategy(snake, delta) desiredAngle = Lerp(0.1, snake.direction, targetAngle) local headX, headY = head.x, head.y local immediateThreat = false for _, other in ipairs(SlitherGame.snakes) do if other ~= snake and other.alive and other.segments[1] then if distanceSq(headX, headY, other.segments[1].x, other.segments[1].y) < (200 * 200) and (#other.segments > #snake.segments) then immediateThreat = true break end end end if immediateThreat and (#snake.segments > snake.initialLength) then snake.currentSpeed = snake.speed * 2.0 * getSlowdownFactor(snake) snake.accelLossTimer = snake.accelLossTimer + delta snake.accelerating = true if snake.accelLossTimer >= 0.1 then snake.accelLossTimer = snake.accelLossTimer - 0.1 if #snake.segments > snake.initialLength then local tail = table.remove(snake.segments) if tail then createPellet(tail.x, tail.y, snake.color, random(3,6), true, true) end end end else snake.currentSpeed = snake.speed * getSlowdownFactor(snake) snake.accelerating = false end end snake.direction = desiredAngle if snake.isPlayer then local newX = head.x + cos(snake.direction) * snake.currentSpeed * delta local newY = head.y + sin(snake.direction) * snake.currentSpeed * delta -- Если змейка игрока выходит за границы, то она погибает if newX < SlitherGame.safeMargin or newX > SlitherGame.boardWidth - SlitherGame.safeMargin or newY < SlitherGame.safeMargin or newY > SlitherGame.boardHeight - SlitherGame.safeMargin then snake.alive = false snake.respawnTime = CurTime() + 0.1 spawnDeathPellets(snake) SlitherGame.lastPlayerPos = { x = head.x, y = head.y } return else table.insert(snake.segments, 1, { x = newX, y = newY }) end else local newX = head.x + cos(snake.direction) * snake.currentSpeed * delta local newY = head.y + sin(snake.direction) * snake.currentSpeed * delta -- Новая логика для ИИ: если выходит за границы, то змейка умирает if newX < SlitherGame.safeMargin or newX > SlitherGame.boardWidth - SlitherGame.safeMargin or newY < SlitherGame.safeMargin or newY > SlitherGame.boardHeight - SlitherGame.safeMargin then snake.alive = false snake.respawnTime = CurTime() + random(1, 5) spawnDeathPellets(snake) return end table.insert(snake.segments, 1, { x = newX, y = newY }) end if snake.grow > 0 then snake.grow = snake.grow - 1 else table.remove(snake.segments) end head = snake.segments[1] local headCellX = floor(head.x / SlitherGame.cellSize) + 1 local headCellY = floor(head.y / SlitherGame.cellSize) + 1 for cx = headCellX - 1, headCellX + 1 do if SlitherGame.pelletGrid[cx] then for cy = headCellY - 1, headCellY + 1 do if SlitherGame.pelletGrid[cx][cy] then local cell = SlitherGame.pelletGrid[cx][cy] for j = #cell, 1, -1 do local pellet = cell[j] if pellet and abs(pellet.x - head.x) <= 100 and abs(pellet.y - head.y) <= 100 then local dx = pellet.x - head.x local dy = pellet.y - head.y if dx * dx + dy * dy < ((snake.thickness/2 + pellet.size) * 1)^2 then snake.grow = snake.grow + 1 removePelletFromGrid(pellet) createPellet() end end end end end end end -- Притягиваем гранулы спереди attractPellets(snake) end -------------------------------- -- Пространственная сетка для коллизий -------------------------------- local SlitherGame_snakeCellSize = 100 local snakeSegmentGrid = {} local function buildSnakeSegmentGrid() for k in pairs(snakeSegmentGrid) do snakeSegmentGrid[k] = nil end for _, snake in ipairs(SlitherGame.snakes) do if snake.alive and snake.segments[1] then local baseThickness = snake.isPlayer and 25 or (25 + ((snake.initialLength - 10) / 20)) snake.thickness = baseThickness + ((#snake.segments - snake.initialLength) / 20) for _, seg in ipairs(snake.segments) do local cellX = floor(seg.x / SlitherGame_snakeCellSize) + 1 local cellY = floor(seg.y / SlitherGame_snakeCellSize) + 1 snakeSegmentGrid[cellX] = snakeSegmentGrid[cellX] or {} snakeSegmentGrid[cellX][cellY] = snakeSegmentGrid[cellX][cellY] or {} table.insert(snakeSegmentGrid[cellX][cellY], { x = seg.x, y = seg.y, thickness = snake.thickness, snake = snake }) end end end end local lastSnakeGridBuildTime = 0 local function updateSnakeSegmentGridIfNeeded() local now = CurTime() if now - lastSnakeGridBuildTime >= 0.1 then buildSnakeSegmentGrid() lastSnakeGridBuildTime = now end end -------------------------------- -- Функция проверки коллизий змей -------------------------------- local function checkSnakeCollisions(viewX, viewY) if not SlitherGame.playerSnake or not SlitherGame.playerSnake.segments[1] then return end for _, snake in ipairs(SlitherGame.snakes) do if snake.alive and snake.segments[1] then local head = snake.segments[1] local dx = head.x - viewX local dy = head.y - viewY if snake.isPlayer or ((dx*dx + dy*dy) <= (SlitherGame.LOD_COLLISION_THRESHOLD * SlitherGame.LOD_COLLISION_THRESHOLD)) then local headRadius = snake.thickness / 2 local searchRadius = headRadius + 50 local minX, maxX = head.x - searchRadius, head.x + searchRadius local minY, maxY = head.y - searchRadius, head.y + searchRadius local cellMinX = floor(minX / SlitherGame_snakeCellSize) + 1 local cellMaxX = floor(maxX / SlitherGame_snakeCellSize) + 1 local cellMinY = floor(minY / SlitherGame_snakeCellSize) + 1 local cellMaxY = floor(maxY / SlitherGame_snakeCellSize) + 1 local collisionFound = false local thresholdSq for cx = cellMinX, cellMaxX do if snakeSegmentGrid[cx] then for cy = cellMinY, cellMaxY do if snakeSegmentGrid[cx][cy] then for _, segInfo in ipairs(snakeSegmentGrid[cx][cy]) do if segInfo.snake ~= snake then local dx2 = segInfo.x - head.x local dy2 = segInfo.y - head.y local segRadius = segInfo.thickness / 2 thresholdSq = (headRadius + segRadius) * 1 thresholdSq = thresholdSq * thresholdSq if dx2 * dx2 + dy2 * dy2 < thresholdSq then collisionFound = true break end end end end if collisionFound then break end end end if collisionFound then break end end if collisionFound then snake.alive = false spawnDeathPellets(snake) if snake.isPlayer then snake.respawnTime = CurTime() + 0.1 SlitherGame.lastPlayerPos = { x = head.x, y = head.y } else snake.respawnTime = CurTime() + random(1, 5) end end end end end end -------------------------------- -- Очистка временных гранул -------------------------------- local function cleanupPellets(viewX, viewY, scrW, scrH) local now = CurTime() for cellX, col in pairs(SlitherGame.pelletGrid) do for cellY, cell in pairs(col) do for i = #cell, 1, -1 do local pellet = cell[i] if pellet.temporary then if (not isVisible(pellet.x, pellet.y, viewX, viewY, scrW, scrH)) and (now - pellet.spawnTime >= 30) then table.remove(cell, i) SlitherGame.pelletCount = SlitherGame.pelletCount - 1 end end end if #cell == 0 then SlitherGame.pelletGrid[cellX][cellY] = nil end end if SlitherGame.pelletGrid[cellX] and table.IsEmpty(SlitherGame.pelletGrid[cellX]) then SlitherGame.pelletGrid[cellX] = nil end end end -------------------------------- -- Интерполяция для отрисовки тела змейки -------------------------------- local function recalcSnakeBody(snake) local body = {} if #snake.segments < 1 then return body end body[1] = snake.segments[1] for i = 2, #snake.segments do local prev, cur = snake.segments[i-1], snake.segments[i] local dx, dy = cur.x - prev.x, cur.y - prev.y local dist = max(1, ceil(sqrt(dx * dx + dy * dy) / 5)) for s = 1, dist do local t = s / dist body[#body + 1] = { x = prev.x + dx * t, y = prev.y + dy * t } end end return body end local function drawSnakeTexture(snake, body, toScreen, thickness) if #body < 3 then return end local lineColor = Color(floor(snake.color.r * 0.7), floor(snake.color.g * 0.7), floor(snake.color.b * 0.7), 120) surface.SetDrawColor(lineColor) local sampleRate = 4 for i = 2, #body - 1, sampleRate do local pPrev, pCurr, pNext = body[i-1], body[i], body[i+1] local tangentX, tangentY = pNext.x - pPrev.x, pNext.y - pPrev.y local mag = sqrt(tangentX * tangentX + tangentY * tangentY) if mag > 0 then tangentX, tangentY = tangentX / mag, tangentY / mag else tangentX, tangentY = cos(snake.direction), sin(snake.direction) end local perpX, perpY = -tangentY, tangentX local lineLength = thickness * 0.4 local sx, sy = toScreen(pCurr.x, pCurr.y) surface.DrawLine(sx - perpX * lineLength, sy - perpY * lineLength, sx + perpX * lineLength, sy + perpY * lineLength) end end -------------------------------- -- Функция отрисовки глаза змейки -------------------------------- function drawEye(eyeCenterX, eyeCenterY, eyeSize, angle) surface.SetDrawColor(255, 255, 255) drawCircle(eyeCenterX, eyeCenterY, eyeSize) local pupilOffset = eyeSize * 0.3 local pupilCenterX = eyeCenterX + cos(angle) * pupilOffset local pupilCenterY = eyeCenterY + sin(angle) * pupilOffset surface.SetDrawColor(0, 0, 0) drawCircle(pupilCenterX, pupilCenterY, eyeSize * 0.5) end -------------------------------- -- Отрисовка статистики (лидерборд и информация) -- Лидерборд переделан по шаблону slither.io: левая колонка для ранга, центральная для никнейма, правая для длины. -------------------------------- local function drawLeaderboard() local leaderboard = {} for _, snake in ipairs(SlitherGame.snakes) do if snake.alive then table.insert(leaderboard, { name = snake.name, score = #snake.segments, color = snake.color }) end end table.sort(leaderboard, function(a, b) return a.score > b.score end) local scrW, scrH = ScrW(), ScrH() local boxWidth = 200 local startX = scrW - 210 local startY = 10 local lineHeight = 20 local headerOffset = 10 draw.SimpleText("Leaderboard", "Trebuchet24", startX + boxWidth / 2, startY, Color(255, 255, 255, 180), TEXT_ALIGN_CENTER, TEXT_ALIGN_TOP) local rankWidth = 10 local gap = 3 local scoreWidth = 50 for i = 1, math.min(10, #leaderboard) do local entry = leaderboard[i] local rowY = startY + headerOffset + i * lineHeight local rankX = startX + rankWidth local nameX = rankX + gap draw.SimpleText(string.format("#%d", i), "Trebuchet18", rankX, rowY, Color(entry.color.r, entry.color.g, entry.color.b, 128), TEXT_ALIGN_RIGHT, TEXT_ALIGN_TOP) draw.SimpleText(entry.name, "Trebuchet18", nameX, rowY, Color(entry.color.r, entry.color.g, entry.color.b, 128), TEXT_ALIGN_LEFT, TEXT_ALIGN_TOP) draw.SimpleText(tostring(entry.score), "Trebuchet18", startX + boxWidth, rowY, Color(entry.color.r, entry.color.g, entry.color.b, 128), TEXT_ALIGN_RIGHT, TEXT_ALIGN_TOP) end end local function drawPlayerRankAndMass() if not SlitherGame.playerSnake then return end local playerScore = #SlitherGame.playerSnake.segments local rank = 1 for _, snake in ipairs(SlitherGame.snakes) do if snake.alive and (#snake.segments > playerScore) then rank = rank + 1 end end local scrW, scrH = ScrW(), ScrH() draw.SimpleText("Length: ", "Trebuchet24", 10, scrH - 60, Color(128, 128, 128, 128), TEXT_ALIGN_LEFT, TEXT_ALIGN_TOP) draw.SimpleText("Rank: ", "Trebuchet24", 10, scrH - 30, Color(128, 128, 128, 128), TEXT_ALIGN_LEFT, TEXT_ALIGN_TOP) local lengthTextWidth = surface.GetTextSize("Length: ") local rankTextWidth = surface.GetTextSize("Rank: ") draw.SimpleText(tostring(playerScore), "Trebuchet24", 10 + lengthTextWidth, scrH - 60, Color(255, 255, 255, 180), TEXT_ALIGN_LEFT, TEXT_ALIGN_TOP) draw.SimpleText(tostring(rank), "Trebuchet24", 10 + rankTextWidth, scrH - 30, Color(255, 255, 255, 180), TEXT_ALIGN_LEFT, TEXT_ALIGN_TOP) end -------------------------------- -- Отрисовка игры (HUDPaint) -------------------------------- hook.Add("HUDPaint", "SlitherGame_Draw", function() if not SlitherGame.gameActive then return end local scrW, scrH = ScrW(), ScrH() local viewX, viewY = SlitherGame.boardWidth / 2, SlitherGame.boardHeight / 2 if SlitherGame.playerSnake and SlitherGame.playerSnake.alive and SlitherGame.playerSnake.segments[1] then viewX, viewY = SlitherGame.playerSnake.segments[1].x, SlitherGame.playerSnake.segments[1].y elseif SlitherGame.lastPlayerPos then viewX = SlitherGame.lastPlayerPos.x viewY = SlitherGame.lastPlayerPos.y end surface.SetDrawColor(30, 30, 30, 255) surface.DrawRect(0, 0, scrW, scrH) local function toScreen(x, y) return (x - viewX) + scrW / 2, (y - viewY) + scrH / 2 end draw.NoTexture() local left, right = viewX - scrW/2 - 50, viewX + scrW/2 + 50 local top, bottom = viewY - scrH/2 - 50, viewY + scrH/2 + 50 local maxCellX = floor((SlitherGame.boardWidth - SlitherGame.safeMargin) / SlitherGame.cellSize) + 1 local maxCellY = floor((SlitherGame.boardHeight - SlitherGame.safeMargin) / SlitherGame.cellSize) + 1 local cellMinX = max(1, floor(left / SlitherGame.cellSize) + 1) local cellMaxX = min(maxCellX, floor(right / SlitherGame.cellSize) + 1) local cellMinY = max(1, floor(top / SlitherGame.cellSize) + 1) local cellMaxY = min(maxCellY, floor(bottom / SlitherGame.cellSize) + 1) for cx = cellMinX, cellMaxX do if SlitherGame.pelletGrid[cx] then for cy = cellMinY, cellMaxY do if SlitherGame.pelletGrid[cx][cy] then for _, pellet in ipairs(SlitherGame.pelletGrid[cx][cy]) do local sx, sy = toScreen(pellet.x, pellet.y) if isVisible(pellet.x, pellet.y, viewX, viewY, scrW, scrH) then surface.SetDrawColor(pellet.color) drawCircle(sx, sy, pellet.size, 16) if pellet.glow then local glowColor = Color(pellet.color.r, pellet.color.g, pellet.color.b, 150) surface.SetDrawColor(glowColor) drawCircle(sx, sy, pellet.size + 3, 16) end end end end end end end for _, snake in ipairs(SlitherGame.snakes) do if snake.alive and snake.segments[1] then local body = recalcSnakeBody(snake) if #body > 0 then local thickness = snake.thickness or 25 surface.SetDrawColor(snake.color) for i = 1, #body do local pt = body[i] if isVisible(pt.x, pt.y, viewX, viewY, scrW, scrH) then local sx, sy = toScreen(pt.x, pt.y) drawCircle(sx, sy, thickness / 2) end end if snake.accelerating then local baseRadius = thickness / 2 local neonColor = Color(min(snake.color.r + 50, 255), min(snake.color.g + 50, 255), min(snake.color.b + 50, 255), 150) local glowStep = 2 -- более частая выборка для плавного эффекта for i = 1, #body, glowStep do local pt = body[i] if isVisible(pt.x, pt.y, viewX, viewY, scrW, scrH) then local sx, sy = toScreen(pt.x, pt.y) surface.SetDrawColor(neonColor.r, neonColor.g, neonColor.b, neonColor.a) drawCircle(sx, sy, baseRadius, 32) end end end local head = snake.segments[1] if isVisible(head.x, head.y, viewX, viewY, scrW, scrH) then local hx, hy = toScreen(head.x, head.y) local angle = snake.direction local rightX, rightY = -sin(angle), cos(angle) local eyeOffset = (snake.thickness or 25) * 0.3 local eyeSeparation = (snake.thickness or 25) * 0.25 local eyeSize = (snake.thickness or 25) * 0.15 drawEye(hx + cos(angle) * eyeOffset + rightX * eyeSeparation, hy + sin(angle) * eyeOffset + rightY * eyeSeparation, eyeSize, angle) drawEye(hx + cos(angle) * eyeOffset - rightX * eyeSeparation, hy + sin(angle) * eyeOffset - rightY * eyeSeparation, eyeSize, angle) local nameColor = snake.isPlayer and Color(snake.color.r, snake.color.g, snake.color.b, snake.nameAlpha or 255) or snake.color draw.SimpleText(snake.name, "Trebuchet18", hx, hy + thickness/2 + 10, nameColor, TEXT_ALIGN_CENTER, TEXT_ALIGN_TOP) end if #body > 2 then drawSnakeTexture(snake, body, toScreen, snake.thickness or 25) end end end end if SlitherGame.playerSnake and SlitherGame.playerSnake.segments[1] then local leftWall, rightWall = SlitherGame.safeMargin, SlitherGame.boardWidth - SlitherGame.safeMargin local topWall, bottomWall = SlitherGame.safeMargin, SlitherGame.boardHeight - SlitherGame.safeMargin local wallLeftScreenX = (leftWall - viewX) + scrW/2 local wallRightScreenX = (rightWall - viewX) + scrW/2 local wallTopScreenY = (topWall - viewY) + scrH/2 local wallBottomScreenY = (bottomWall - viewY) + scrH/2 if wallLeftScreenX > 0 then surface.SetDrawColor(80, 0, 0, 200) -- Темно-бордовый цвет с прозрачностью surface.DrawRect(0, 0, wallLeftScreenX, scrH) end if wallRightScreenX < scrW then surface.SetDrawColor(80, 0, 0, 200) surface.DrawRect(wallRightScreenX, 0, scrW - wallRightScreenX, scrH) end if wallTopScreenY > 0 then surface.SetDrawColor(80, 0, 0, 200) surface.DrawRect(0, 0, scrW, wallTopScreenY) end if wallBottomScreenY < scrH then surface.SetDrawColor(80, 0, 0, 200) surface.DrawRect(0, wallBottomScreenY, scrW, scrH - wallBottomScreenY) end end local miniW, miniH = 200, 200 local miniX, miniY = scrW - miniW - 10, scrH - miniH - 10 surface.SetDrawColor(20, 20, 20, 220) surface.DrawRect(miniX, miniY, miniW, miniH) surface.SetDrawColor(100, 100, 100, 255) surface.DrawOutlinedRect(miniX, miniY, miniW, miniH) local scaleX, scaleY = miniW / SlitherGame.boardWidth, miniH / SlitherGame.boardHeight for _, snake in ipairs(SlitherGame.snakes) do if snake.alive and snake.segments[1] then local head = snake.segments[1] local mx = miniX + head.x * scaleX local my = miniY + head.y * scaleY if snake.isPlayer then -- Игрок всегда отображается белым на миникарте surface.SetDrawColor(255, 255, 255, 255) else surface.SetDrawColor(128, 128, 128, 255) end surface.DrawRect(mx - 2, my - 2, 4, 4) end end if SlitherGame.playerSnake and not SlitherGame.playerSnake.alive then draw.SimpleText("You lost! Press R to revive", "Trebuchet24", scrW / 2, scrH / 2, Color(255, 0, 0), TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER) end drawLeaderboard() drawPlayerRankAndMass() draw.SimpleText("PRESS BACKSPACE TO CLOSE", "Trebuchet24", 10, 10, Color(255, 255, 255)) end) -------------------------------- -- Управление камерой и вводом -- Отключаем прыжок: в хуке StartCommand удаляем флаг IN_JUMP. -------------------------------- hook.Add("StartCommand", "Slither_CaptureInput", function(ply, cmd) if SlitherGame.slitherActive then cmd:ClearMovement() local buttons = cmd:GetButtons() buttons = bit.band(buttons, bit.bnot(IN_JUMP)) cmd:SetButtons(buttons) cmd:SetUpMove(0) end end) local function AddSlitherHooks() hook.Add("CalcView", "Slither_CalcView", function(ply, pos, angles, fov) if SlitherGame.slitherActive then local view = {} if SlitherGame.playerSnake and SlitherGame.playerSnake.alive and SlitherGame.playerSnake.segments[1] then view.origin = Vector(SlitherGame.playerSnake.segments[1].x, SlitherGame.playerSnake.segments[1].y, 1000) elseif SlitherGame.lastPlayerPos then view.origin = Vector(SlitherGame.lastPlayerPos.x, SlitherGame.lastPlayerPos.y, 1000) else view.origin = Vector(SlitherGame.boardWidth/2, SlitherGame.boardHeight/2, 1000) end view.angles = Angle(0, 0, 0) view.fov = fov view.drawviewer = false return view end end) hook.Add("PreDrawViewModel", "Slither_PreDrawViewModel", function() if SlitherGame.slitherActive then return false end end) hook.Add("PreDrawPlayerHands", "Slither_PreDrawPlayerHands", function() if SlitherGame.slitherActive then return false end end) hook.Add("HUDShouldDraw", "Slither_HideDefaultHUD", function(name) if SlitherGame.slitherActive then local hideElements = { ["CHudHealth"] = true, ["CHudBattery"] = true, ["CHudAmmo"] = true, ["CHudSecondaryAmmo"] = true, ["CHudCrosshair"] = true, } if hideElements[name] then return false end end end) end -------------------------------- -- Основное обновление игры -------------------------------- local function updateGame(delta) if not SlitherGame.gameActive then return end -- Обновление эффекта затухания ника игрока (3 секунды) if SlitherGame.playerSnake and SlitherGame.playerSnake.isPlayer then local elapsed = CurTime() - SlitherGame.playerSnake.spawnTime SlitherGame.playerSnake.nameAlpha = math.max(0, 255 - (elapsed / 3) * 255) end local playerX, playerY = SlitherGame.boardWidth / 2, SlitherGame.boardHeight / 2 if SlitherGame.playerSnake and SlitherGame.playerSnake.segments[1] then playerX, playerY = SlitherGame.playerSnake.segments[1].x, SlitherGame.playerSnake.segments[1].y end for i = #SlitherGame.snakes, 1, -1 do local snake = SlitherGame.snakes[i] if snake then if snake.alive then updateSnakeFull(snake, delta, playerX, playerY) else if snake.isPlayer and CurTime() >= snake.respawnTime then if input.IsKeyDown(KEY_R) then local newSnake = createSnake(true) SlitherGame.playerSnake = newSnake SlitherGame.snakes[i] = newSnake end elseif not snake.isPlayer and CurTime() >= snake.respawnTime then SlitherGame.snakes[i] = createSnake(false) end end end end while SlitherGame.pelletCount < SlitherGame.targetPelletCount do createPellet() end local scrW, scrH = ScrW(), ScrH() local viewX, viewY = SlitherGame.boardWidth / 2, SlitherGame.boardHeight / 2 if SlitherGame.playerSnake and SlitherGame.playerSnake.alive and SlitherGame.playerSnake.segments[1] then viewX, viewY = SlitherGame.playerSnake.segments[1].x, SlitherGame.playerSnake.segments[1].y elseif SlitherGame.lastPlayerPos then viewX = SlitherGame.lastPlayerPos.x viewY = SlitherGame.lastPlayerPos.y end cleanupPellets(viewX, viewY, scrW, scrH) updateSnakeSegmentGridIfNeeded() checkSnakeCollisions(viewX, viewY) end -------------------------------- -- Запуск/выход из игры -------------------------------- local function initGame() SlitherGame.pelletGrid = {} SlitherGame.pelletCount = 0 for i = 1, SlitherGame.targetPelletCount do createPellet() end SlitherGame.snakes = {} SlitherGame.gameActive = true SlitherGame.slitherActive = true SlitherGame.lastPlayerPos = nil SlitherGame.playerSnake = createSnake(true) table.insert(SlitherGame.snakes, SlitherGame.playerSnake) for i = 1, SlitherGame.aiSnakeCount do table.insert(SlitherGame.snakes, createSnake(false)) end AddSlitherHooks() end local function RemoveSlitherHooks() hook.Remove("CalcView", "Slither_CalcView") hook.Remove("PreDrawViewModel", "Slither_PreDrawViewModel") hook.Remove("PreDrawPlayerHands", "Slither_PreDrawPlayerHands") hook.Remove("HUDShouldDraw", "Slither_HideDefaultHUD") end local function exitGame() SlitherGame.gameActive = false SlitherGame.slitherActive = false SlitherGame.snakes = {} for k in pairs(SlitherGame.pelletGrid) do SlitherGame.pelletGrid[k] = nil end SlitherGame.pelletCount = 0 RemoveSlitherHooks() end -------------------------------- -- Обновление (Think) -------------------------------- hook.Add("Think", "SlitherGame_Update", function() if input.IsKeyDown(KEY_BACKSPACE) then exitGame() end; if not SlitherGame.gameActive then return end local curTime = CurTime() local delta = curTime - SlitherGame.lastUpdateTime if delta >= SlitherGame.updateInterval then updateGame(delta) SlitherGame.lastUpdateTime = curTime end end) initGame()