Facebook
From Na, 2 Months ago, written in Lua.
Embed
Download Paste or View Raw
Hits: 178
  1. --!strict
  2. local CollectionService = game:GetService("CollectionService")
  3. local Players = game:GetService("Players")
  4. local ReplicatedStorage = game:GetService("ReplicatedStorage")
  5.  
  6. local Network = require(ReplicatedStorage.Shared.Network)
  7. local Signal = require(ReplicatedStorage.Shared.Signal)
  8. local SecureCast = require(ReplicatedStorage.Shared.Mods.SecureCast)
  9. local EquipmentLibrary = require(ReplicatedStorage.Shared.Library.Equipment)
  10.  
  11. export type Shooter = {
  12.  __index: Shooter,
  13.  ID: string,
  14.  Player: Player?,
  15.  Character: Model?,
  16.  Settings: EquipmentLibrary.ShooterInfo,
  17.  Tool: Tool?,
  18.  ShootOrigin: Attachment,
  19.  
  20.  Magazine: {
  21.   Size: number,
  22.   Loaded: number,
  23.   Stored: number,
  24.  },
  25.  
  26.  Clock: {
  27.   Trigger: number,
  28.   Reload: number,
  29.  },
  30.  
  31.  Destroyed: boolean,
  32.  
  33.  new: (tool: Tool, shooterInfo: EquipmentLibrary.ShooterInfo) -> Shooter,
  34.  CanFire: (self: Shooter) -> boolean,
  35.  Reload: (self: Shooter, phase: number) -> boolean,
  36.  Activate: (self: Shooter, origin: Vector3, directions: { Vector3 }, timestamp: number) -> boolean,
  37.  Destroy: (self: Shooter) -> (),
  38. }
  39.  
  40. local Shooter = {} :: Shooter
  41. Shooter.__index = Shooter
  42.  
  43. local MAXIMUM_LATENCY = 0.8 -- 800 ms
  44.  
  45. --==========================-- Private Functions --==========================--
  46.  
  47. -- Applies damage to humanoid
  48. local function HitHumanoid(
  49.  humanoid: Humanoid,
  50.  damage: number,
  51.  defaultRange: number,
  52.  distanceCovered: number?,
  53.  byPlayer: Player?
  54. )
  55.  if humanoid.Health > 0 then
  56.   local distance = math.max(0, distanceCovered or 1)
  57.   local range = defaultRange or distance
  58.   local baseDamage = damage or 0
  59.   local damageDamper = math.min(1, range / distance)
  60.   local effectiveDamage = math.round(damageDamper * baseDamage * 100) / 100
  61.  
  62.   humanoid:TakeDamage(effectiveDamage)
  63.  
  64.   local model = humanoid.Parent :: Model
  65.   Signal.MobDamaged:Fire(byPlayer, model, effectiveDamage)
  66.  
  67.   if humanoid.Health <= 0 then
  68.    Signal.MobKilled:Fire(byPlayer, model)
  69.   end
  70.  
  71.   -- TODO: Send event to client
  72.  end
  73. end
  74.  
  75. local function GetHit(
  76.  caster: Player,
  77.  direction: Vector3,
  78.  instance: Instance,
  79.  normal: Vector3,
  80.  position: Vector3,
  81.  radius: number,
  82.  filterInstances: { Instance }
  83. ): { Humanoid }
  84.  local humanoids = {}
  85.  if instance and instance.Parent then
  86.   -- Temporary damage logic
  87.   if radius > 0 then
  88.    local overlapParams = OverlapParams.new()
  89.    overlapParams.FilterType = Enum.RaycastFilterType.Exclude
  90.    overlapParams.FilterDescendantsInstances = filterInstances
  91.  
  92.    local parts = workspace:GetPartBoundsInRadius(position, radius, overlapParams)
  93.    local foundHumanoids = {}
  94.    for _, part: BasePart in pairs(parts) do
  95.     -- Humanoid & health check
  96.     local humanoid = part.Parent and part.Parent:FindFirstChild("Humanoid") :: Humanoid?
  97.     if not humanoid or humanoid.Health > 0 then
  98.      continue
  99.     end
  100.  
  101.     -- Already found check
  102.     if foundHumanoids[humanoid] then
  103.      continue
  104.     end
  105.  
  106.     -- Same player & same team check
  107.     local humanoidPlayer: Player? = Players:GetPlayerFromCharacter(part.Parent)
  108.     if humanoidPlayer and (caster == humanoidPlayer or caster.Team == humanoidPlayer.Team) then
  109.      continue
  110.     end
  111.  
  112.     foundHumanoids[humanoid] = humanoid
  113.    end
  114.  
  115.    for _, humanoid: Humanoid in pairs(foundHumanoids) do
  116.     table.insert(humanoids, humanoid)
  117.    end
  118.   else
  119.    local humanoid = instance.Parent:FindFirstChild("Humanoid") :: Humanoid?
  120.    if humanoid then
  121.     table.insert(humanoids, humanoid)
  122.    end
  123.   end
  124.  end
  125.  
  126.  return humanoids
  127. end
  128.  
  129. --==========================-- Public Functions --==========================--
  130.  
  131. function Shooter.new(tool: Tool, shooterInfo: EquipmentLibrary.ShooterInfo): Shooter
  132.  local self: Shooter = setmetatable({} :: any, Shooter)
  133.  self.ID = tool:GetAttribute("SID")
  134.  self.Player = nil
  135.  self.Character = nil
  136.  self.Settings = shooterInfo
  137.  
  138.  self.Tool = tool
  139.  self.ShootOrigin = tool:FindFirstChild("ShootOrigin", true) :: Attachment
  140.  
  141.  self.Magazine = {
  142.   Size = self.Settings.Magazine.Size,
  143.   Loaded = self.Settings.Magazine.Size,
  144.   Stored = self.Settings.Magazine.Stored,
  145.  }
  146.  
  147.  self.Clock = {
  148.   Trigger = 0,
  149.   Reload = 0,
  150.  }
  151.  
  152.  return self
  153. end
  154.  
  155. function Shooter:CanFire(): boolean
  156.  local toolExists = (self.Tool and self.Tool.Parent) and true or false
  157.  
  158.  local now = workspace:GetServerTimeNow()
  159.  local triggerCooldownOver = now - self.Clock.Trigger >= self.Settings.Cooldown
  160.  local reloadOver = now - self.Clock.Reload >= self.Settings.Reload.Time
  161.  return toolExists and triggerCooldownOver and reloadOver
  162. end
  163.  
  164. function Shooter:Reload(phase: number): boolean
  165.  if phase == 1 then
  166.   return self.Magazine.Loaded < self.Magazine.Size and self.Magazine.Stored > 0
  167.  end
  168.  
  169.  local count = self.Settings.Reload.Count
  170.  local addCount = count > 0 and count or self.Settings.Magazine.Size
  171.  local actualCount = math.min(self.Settings.Magazine.Size - self.Magazine.Loaded, addCount)
  172.  
  173.  if self.Magazine.Stored - actualCount >= 0 then
  174.   self.Magazine.Stored -= actualCount
  175.   self.Magazine.Loaded = math.min(self.Settings.Magazine.Size, self.Magazine.Loaded + addCount)
  176.   return true
  177.  end
  178.  
  179.  self.Clock.Reload = workspace:GetServerTimeNow()
  180.  
  181.  return false
  182. end
  183.  
  184. function Shooter:Activate(origin: Vector3, directions: { Vector3 }, timestamp: number): boolean
  185.  local prerequisitesExist = (origin and directions and timestamp) and self.Character and true or false
  186.  
  187.  if prerequisitesExist and self:CanFire() then
  188.   if self.Magazine.Loaded > 0 then
  189.    local caster: Player? = self.Player
  190.    local castType = "Bullet"
  191.    self.Clock.Trigger = workspace:GetServerTimeNow()
  192.    self.Magazine.Loaded = math.max(0, self.Magazine.Loaded - 1)
  193.  
  194.    for _count, direction: Vector3 in directions do
  195.     -- Calculate values for latency compensation
  196.     local currentTime = os.clock()
  197.     local interpolation = (self.Player and self.Player:GetNetworkPing() or 0)
  198.      + SecureCast.Settings.Interpolation
  199.     local latency = (workspace:GetServerTimeNow() - timestamp)
  200.  
  201.     -- Abort if client latency is too high; simulation will be too innacurate
  202.     if (latency < 0) or (latency > MAXIMUM_LATENCY) then
  203.      return false
  204.     end
  205.  
  206.     -- Abort if client-provided origin position is too far away; potentially cheating
  207.     local shootOrigin = self.Tool and self.Tool:FindFirstChild("ShootOrigin", true) :: Attachment?
  208.     if
  209.      shootOrigin
  210.      and shootOrigin:IsA("Attachment")
  211.      and (shootOrigin.WorldPosition - origin).Magnitude > 5
  212.     then
  213.      return false
  214.     end
  215.  
  216.     local modifier = {
  217.      Loss = self.Settings.Ricochet.Loss,
  218.      Power = self.Settings.Shot.Power,
  219.      Velocity = self.Settings.Shot.Velocity,
  220.      Angle = self.Settings.Ricochet.Angle,
  221.      Gravity = -self.Settings.Shot.Gravity,
  222.     }
  223.  
  224.     -- Replicate the projectile to all other clients
  225.     if caster then
  226.      Network.BulletFired:Server():FireAllExcept(caster, caster, castType, origin, direction, modifier)
  227.     else
  228.      Network.BulletFired:Server():FireAll(caster, castType, origin, direction, modifier)
  229.     end
  230.  
  231.     -- Set serverside raycast filters
  232.     local raycastParams = RaycastParams.new()
  233.     raycastParams.FilterType = Enum.RaycastFilterType.Exclude
  234.     local filterInstances = { workspace:FindFirstChild("Stain") }
  235.  
  236.     local character = self.Character :: Model
  237.     if character then
  238.      if character.Parent and character.Parent:IsA("Folder") then
  239.       table.insert(filterInstances, character.Parent)
  240.      else
  241.       table.insert(filterInstances, character)
  242.      end
  243.     end
  244.     raycastParams.FilterDescendantsInstances = filterInstances
  245.  
  246.     -- Read cast events
  247.     local bindable = Instance.new("BindableEvent")
  248.     bindable.Event:Connect(function(castType: string, eventName: string, ...)
  249.      local data = { ... }
  250.      if eventName == "OnDestroyed" then
  251.       bindable:Destroy()
  252.      elseif eventName == "OnImpact" then
  253.       local caster, dir, instance, normal, position, _material = table.unpack(data)
  254.       local humanoids = GetHit(
  255.        caster,
  256.        dir,
  257.        instance,
  258.        normal,
  259.        position,
  260.        self.Settings.AreaOfEffect,
  261.        raycastParams.FilterDescendantsInstances
  262.       )
  263.  
  264.       for _, humanoid in humanoids do
  265.        HitHumanoid(humanoid, self.Settings.Shot.Damage, self.Settings.Shot.Range, nil, self.Player)
  266.       end
  267.  
  268.       if not self.Player and CollectionService:HasTag(instance.Parent, "BASE") then
  269.        local health = instance.Parent:FindFirstChild("Health") :: NumberValue?
  270.        if health then
  271.         health.Value = math.max(0, health.Value - self.Settings.Shot.Damage)
  272.        end
  273.       end
  274.      elseif eventName == "OnIntersection" then
  275.       local caster, dir, characterPart: string, victim: Player, position = table.unpack(data)
  276.       local humanoid = victim.Character and victim.Character:FindFirstChild("Humanoid") :: Humanoid?
  277.       if humanoid then
  278.        HitHumanoid(humanoid, self.Settings.Shot.Damage, self.Settings.Shot.Range, nil, self.Player)
  279.       end
  280.      end
  281.     end)
  282.  
  283.     -- Define cast properties
  284.     modifier.RaycastFilter = raycastParams
  285.     modifier.OnImpact = bindable
  286.     modifier.OnDestroyed = bindable
  287.     modifier.OnIntersection = bindable
  288.  
  289.     -- Cast the projectile within server-side simulation
  290.     SecureCast.Cast(
  291.      caster,
  292.      "Bullet",
  293.      origin,
  294.      direction,
  295.      currentTime - latency - interpolation,
  296.      nil,
  297.      modifier
  298.     )
  299.    end
  300.  
  301.    return true
  302.   end
  303.  end
  304.  return false
  305. end
  306.  
  307. function Shooter:Destroy()
  308.  if not self.Destroyed then
  309.   self.Destroyed = true
  310.  
  311.   self.Player = nil
  312.   self.Character = nil
  313.   self.Tool = nil
  314.  end
  315. end
  316.  
  317. return Shooter
  318.