Skip to content

🎱 Billiards

Documentation relating to the spooni_billiards.

1. Installation

spooni_billiards works Standalone.

To install spooni_billiards:

  • Download the resource
  • Drag and drop the resource into your resources folder
    • spooni_billiards
  • Add this ensure in your server.cfg
      ensure spooni_billiards
  • Now you can configure and translate the script as you like
    • config.lua
  • At the end, restart the server

If you have any problems, you can always open a ticket in the Spooni Discord.

2. Usage

With this script, players can challenge each other to 1v1 pool matches directly on placeable billiard tables. The script features a full physics simulation, wager/betting support, practice mode, and a weekly leaderboard. Tables can be placed as inventory items and fully configured via config.lua.

3. For developers

Config.lua
lua
Config = {}

-- Main language used by the resource.
Config.locale = "en"

-- How close a player must be to open the table menu.
Config.tableInteractDistance = 2.0

Config.ui = Config.ui or {}
-- Shows a small toast when table sync finishes.
Config.ui.showTablesSyncedToast = false
-- Extra milliseconds added to every NUI notification.
Config.ui.notificationExtraDurationMs = 1500
-- Notification side: "right" or "left".
Config.ui.notificationSide = "left" -- right or left

Config.input = {
  -- Optional native control hash for table interaction. Nil = keymapping/raw key only.
  interactControl = nil,
  -- Label shown in texts for table interaction.
  interactLabel = "Y",
  -- Optional second interaction control hash. Nil = disabled.
  menuFallbackControl = nil,
  -- Label for the fallback interaction key if used.
  menuFallbackLabel = nil,
  -- Optional native control hash for HUD toggle. Nil = keymapping/raw key only.
  toggleHudControl = nil,
  -- Label shown in texts for HUD toggle.
  toggleHudLabel = "U",
  -- Native control hash used to pick the table up.
  pickupTableControl = 0x760A9C6F, -- G
  -- Label shown in texts for pickup.
  pickupTableLabel = "G"
}

Config.admin = {
  commands = {
    -- Allows the create-table admin command.
    createTableEnabled = false,
    -- Allows the delete-table admin command.
    deleteTableEnabled = false,
    -- Allows the list-tables admin command.
    listTablesEnabled = false,
    -- Allows the purge-tables admin command.
    purgeTablesEnabled = false,
    -- Allows the give-table-item admin command.
    giveTableItemEnabled = true,
  }
}

Config.tableItem = {
  -- Enables placing/picking up tables as inventory items.
  enabled = true,
  -- Inventory item name used by the framework.
  itemName = "pool_table_item",
  -- Closes inventory automatically after using the item.
  closeInventoryOnUse = true,

  placement = {
    -- Max distance between player and preview table while placing.
    maxDistanceFromPlayer = 6.0,
    -- Min distance required from other tables.
    minDistanceFromOtherTables = 3.0,
    -- Move speed of the placement preview.
    moveSpeed = 2.0,
    -- Rotation speed of the placement preview.
    rotateSpeedDeg = 90.0,
    -- Preview transparency.
    previewAlpha = 180,
    -- Initial preview distance in front of the player.
    startForwardDistance = 1.8,
    -- Initial preview height offset.
    startHeightOffset = 0.0,
    confirmAnimation = {
      -- Plays a short confirm animation when placing.
      enabled = true,
      -- Scenario used for the confirm animation.
      scenario = "WORLD_HUMAN_CROUCH_INSPECT",
      -- How long the confirm animation lasts.
      durationMs = 900,
      -- Small turn-to-table delay before placing.
      turnToTableMs = 250,
    },
  },

  pickup = {
    -- Enables turning placed tables back into the item.
    enabled = true,
    -- If true, anyone can pick the table up.
    allowAnyPlayer = false,
    -- Distance required to pick the table up.
    interactDistance = 2.5,
  }
}

Config.match = {
  -- Max allowed player distance from the table to start/accept matches.
  maxPlayerDistanceToTable = 4.0,

  -- Max distance for nearby non-participants to receive spectator sync.
  spectatorSyncDistance = 12.0,

  -- Distance that cancels an active match if a player moves too far away.
  cancelDistance = 4.0,

  -- How many seconds an invite stays valid.
  inviteExpiresSeconds = 30,

  -- Enables solo practice mode.
  allowPracticeMode = true,

  -- How long the quick rematch button stays available after a match.
  rematchWindowSeconds = 45,

  -- How often the server checks cancel conditions.
  cancelCheckIntervalMs = 1500,
  -- Grace time before confirming a distance/disconnect cancel.
  cancelGraceSeconds = 2
}

Config.bet = {
  -- Enables wager matches.
  enabled = true,
  -- Allows selecting "no bet" in 1v1.
  allowNoBet = true,
  -- Prints extra bet/economy debug logs in console.
  debug = true,
  -- Max bet amount accepted by the script.
  maxAmount = 500,
  -- If true, both stakes are charged when the match starts.
  chargeOnMatchStart = true,
  -- Extra % of offender stake given to the player who stayed in range.
  rangeCancelRewardPercent = 25,
  -- Bet buttons shown in the UI.
  defaultAmounts = { 10, 20, 50, 100, 200, 500 }
}

Config.cueParking = {
  -- Enables placing the cue on the table edge during a match.
  enabled = false,

  -- How far outside the rail the cue tip is placed.
  edgeOutward = 0.31,

  -- Extra local Z offset for the cue tip.
  tipLocalZOffset = -0.60,

  -- Distance from cue tip to butt while parked.
  buttDistance = 0.01,

  -- Extra offset above ground for the cue butt.
  buttGroundOffset = 0.58,

  -- Max distance from table edge to allow cue parking.
  edgeThreshold = 0.85,

  -- "edge" uses rail normal, "center" points out from table center.
  outwardMode = "edge",

  -- Fine yaw correction for parked cue.
  yawOffsetDeg = 2.0,
  -- Fine pitch correction for parked cue.
  pitchOffsetDeg = 0.0,
}

Config.db = {
  -- SQL table used for placed pool tables.
  tableName = "pool_tables",
  -- SQL table used for player profiles/stats.
  playerTableName = "pool_players",
  -- SQL table used for weekly ranking stats.
  weeklyTableName = "pool_weekly_stats"
}

Config.models = {
  -- World model used for the table itself.
  tableModel = "p_ambfloorplantravel01x",
  -- Model used for the cue prop.
  cueModel   = "p_ambfloorplantent01x",
  ballModels = {
    -- Per-ball model overrides.
    [1]  = "p_ammobox01x",
    [2]  = "p_ammobox02x",
    [3]  = "p_gravemother01x",
    [4]  = "p_carcasshorse02x",
    [5]  = "p_graveplaque01x",
    [6]  = "p_carcasssnake01x",
    [7]  = "p_hatbox05x",
    [8]  = "p_vg_tin09",
    [9]  = "p_vg_tin04",
    [10] = "prop_mk_arrow_3d",
    [11] = "prop_mk_num_2",
    [12] = "prop_mk_num_3",
    [13] = "prop_mk_num_4",
    [14] = "prop_mk_num_0",
    [15] = "prop_mk_num_1",
  },
  -- Legacy fallback pattern if specific ball models are not set.
  ballModelPattern = nil,
  -- Model used for the cue ball.
  whiteBallModel   = "p_ambfloorplandecor01x",
}

Config.spawn = {
  -- Global Z offset applied when spawning the table model.
  tableZOffset = -1.0
}

Config.balls = {
  -- Ball collision/render radius.
  radius = 0.028,
  -- Distance between balls in the starting rack.
  rackSpacing = 0.047,
  visualZOffsets = {
    -- Per-ball visual height tweaks.
    [1] = 0.0015,
    [2] = 0.0015,
  },
  forceVisibleNumbers = {
    -- Balls forced visible locally for render stability.
    [1] = true,
    [2] = true,
  },

  -- Table local axis used as "forward" for the rack.
  forwardAxis = "x",

  layout = {
    -- Rack center position forward on the table.
    rackCenterForward = 0.40,
    -- Cue ball spawn position forward on the table.
    cueForward = -0.53,
    -- Side offset for rack/cue layout.
    side = 0.0
  },

  -- Uses raycast to detect cloth height instead of only local offsets.
  useRaycastForClothZ = true,

  -- Fallback cloth local Z if raycast is disabled/fails.
  clothLocalZ = 0.753,

  -- Small extra lift/drop applied to all balls on the cloth.
  surfaceLift = -0.0015
}

Config.cue = {
  -- Enables the hand-attached cue prop.
  enabled = true,

  -- Ped bone where the cue is attached.
  boneName = "SKEL_R_HAND",

  -- Cue position offset relative to the bone.
  offset = { x = 0.22, y = -0.3, z = -0.058 },

  -- Cue rotation relative to the bone.
  rotation = { x = -85.0, y = -110.0, z = 4.0 }
}

Config.shot = {
  -- Native control hash used to enter/leave aiming mode.
  toggleControl = 0xE30CD707,
  -- Label shown in texts for aiming mode.
  toggleLabel = "R",

  -- How long the cue is frozen visually after a shot.
  cueFreezeAfterShotMs = 1400,

  -- How far behind the cue ball the player must stand to aim.
  aimSpotBackOffset = 0.85,
  -- Radius around the aim spot that counts as valid.
  aimSpotRadius = 1.0,

  -- Base aiming camera FOV.
  cameraFov = 55.0,
  -- Offset of the aiming camera.
  cameraOffset = { x = 0.03, y = 0.12, z = 0.02 },

  -- Inverts horizontal look input while aiming.
  invertLookX = true,
  -- Inverts vertical look input while aiming.
  invertLookY = false,
  -- Horizontal look sensitivity.
  lookSensitivityX = 3.0,
  -- Vertical look sensitivity.
  lookSensitivityY = 2.5,

  multiRail = {
    -- Enables switching aim side depending on rail position.
    enabled = true,
    -- Max rail distance for dynamic aim spot selection.
    maxRailDistance = 0.80
  },

  aimCamera = {
    -- Enables the dedicated aiming camera.
    enabled = true,
    -- Camera distance behind the cue butt.
    backFromButt = 0.18,
    -- Camera height offset.
    up = 0.40,
    -- Camera side offset.
    side = 0.0,
    -- How far ahead the camera looks along the cue.
    lookAtForward = 0.60,
    -- Aiming camera FOV override.
    fov = 55.0,
    -- Additional down pitch for the aiming camera.
    pitchDownDeg = 0.0,
  },

  cueVisual = {
    -- Enables the separate aiming cue visual.
    enabled = true,
    -- Optional custom cue model override.
    model = nil,
    -- Total visual cue length.
    length = 1.45,
    -- Gap between cue tip and cue ball.
    tipGap = 0.10,
    -- Downward pitch of the cue visual.
    pitchDownDeg = -10.0,
    -- Vertical offset of the cue visual.
    upOffset = 0.099,
    -- Side offset of the cue visual.
    sideOffset = 0.0,
    -- Forward axis of the cue model.
    modelForwardAxis = "x", -- "x" | "y"
    -- Flips forward direction if needed.
    modelForwardSign = 1.0, -- 1.0 or -1.0 (flip if needed)
    -- Distance from model origin to cue tip.
    tipFromOrigin = 1.90,
    -- Heading correction for the cue visual.
    headingOffsetDeg = 0.0,
    -- Extra rotation on X.
    rotX = 0.0,
    -- Extra rotation on Y.
    rotY = 0.0,
    -- Extra rotation on Z.
    rotZ = 0.0,

    -- Hides the hand cue while the aiming cue visual is active.
    hideHandCueWhileAiming = true,
    -- Sync frequency for remote cue visuals.
    syncHz = 30
  },
}

Config.physics = {
  -- Simulation tick rate.
  tickHz = 120,
  -- Max substeps processed per frame.
  maxSubSteps = 8,
  -- Snapshot sync rate for remote clients.
  snapshotHz = 20,

  renderFix = {
    -- Extra render stability for spawned ball props.
    enabled = true,
    -- Forces prerender on tracked entities.
    alwaysPrerender = true,
    -- Max distance to keep render fixes active.
    cullingRadius = 100.0,
  },

  -- Speed below which a ball starts trying to sleep.
  sleepSpeed = 0.03,
  -- Time a slow ball must remain slow before sleeping.
  sleepTimeMs = 250,

  -- Rolling friction / deceleration.
  rollingDecel = 0.40,
  -- Final low-speed clamp to stop micro-movement.
  minSpeedClamp = 0.0005,

  -- Bounce factor for ball-ball collisions.
  restitutionBall = 0.965,
  -- Bounce factor for ball-rail collisions.
  restitutionRail = 0.88,

  -- Allowed overlap before correction.
  overlapSlop = 0.0005,
  -- Strength of overlap correction.
  overlapCorrection = 0.85,

  -- Pocket detection radius.
  pocketRadius = 0.065,
  -- Pocket inset used by physics.
  pocketInset = 0.018,
  -- Side pocket horizontal shift tweak.
  pocketSideShift = 0.033,

  -- Pocket drop animation duration.
  pocketDropMs = 260,
  -- Pocket drop animation depth.
  pocketDropDepth = 0.20,

  -- Delay before cue ball respawn after pocketing.
  cueRespawnDelayMs = 900,

  -- Max shot speed.
  shotMaxSpeed = 4.6,
  -- Min shot speed.
  shotMinSpeed = 0.13,
  -- Power response curve.
  powerCurve = 1.20,

  -- Max pullback distance of the cue.
  cuePullbackMax = 0.32,
  -- Pullback speed when charging.
  cuePullbackInSpeed = 6.0,
  -- Return speed when releasing charge.
  cuePullbackOutSpeed = 8.0,
  -- Margin to keep cue inside valid bounds.
  cuePullbackBoundMargin = 0.18,

  -- Forward cue stroke distance when shooting.
  cueForwardStroke = 0.90,
  -- Follow-through duration after stroke.
  cueFollowThroughMs = 30,

  bounds = {
    -- Forward/backward local limits of the playable area.
    minForward = -0.62 - 0.36,
    maxForward =  0.62 + 0.36,
    -- Left/right local limits of the playable area.
    minSide    = -0.30 - 0.185,
    maxSide    =  0.30 + 0.185,
  },

  debug = {
    -- Draws debug table bounds.
    drawBounds = false,
    -- Draws debug ball ids.
    drawBallIds = false,
    -- Draws debug pocket positions.
    drawPockets = false,
  },

  -- Extra friction applied on ball-ball impact.
  ballBallFriction = 0.10,
  -- Lower restitution used for shallow ball collisions.
  restitutionBallGrazing = 0.90,
}

Config.audio = {
  -- Enables spatial pool sounds.
  enabled = true,

  -- Max distance where sounds can be heard.
  radius = 6.0,

  volumes = {
    -- Cue strike volume.
    cue = 0.55,
    -- Hard impact volume.
    hard = 0.65,
    -- Soft impact volume.
    soft = 0.45,
    -- Rail impact volume.
    rail = 0.40,
    -- Pocketed sound volume.
    pocket = 0.60
  },

  -- Min ticks between repeated sounds.
  minInterval = 4,
  -- Time window to mix multiple impacts together.
  impactMixWindowMs = 12,

  -- Minimum speed required to play an impact sound.
  minImpactSpeed = 0.18,

  -- Speed threshold considered a hard impact.
  hardImpactSpeed = 1.10
}

Config.ballInHand = {
  -- Enables manual ball-in-hand placement mode.
  enabled = false,

  -- Main confirm control.
  confirmControl1 = 0x07CE1E61, -- INPUT_ATTACK (left click)
  -- Secondary confirm control.
  confirmControl2 = 0xC7B5340A, -- ENTER (common in frontend)
  -- Main cancel control.
  cancelControl1  = 0x156F7119, -- INPUT_FRONTEND_CANCEL (ESC/B)
  -- Secondary cancel control.
  cancelControl2  = 0x4AF4D473, -- often "DELETE" in some mappings

  -- Placement move speed.
  moveSpeed = 0.55,
  -- Fine movement multiplier.
  fineMultiplier = 0.25,

  -- Placement camera height.
  camHeight = 2.5,
  -- Placement camera FOV.
  camFov = 65.0,

  -- Safety margin inside table bounds.
  boundsMargin = 0.02,

  -- Min distance required from other balls.
  minDistanceToBalls = 0.075,
}