🎱 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,
}