A PVPVE survival gamemode for Garry's Mod.
It's you (and your friends?) versus a variety of relentless enemies. Get close to hunting NPCs to earn score, then spend it all in the shop on weapons, beartraps, innate upgrades, and more.
But here's the twist: the fun really begins when you die. As a ghost, you unlock a whole new shop selection. Lock doors, place traps for your friends, or build a tempting supply room rigged with explosive barrels!
๐ฎ Steam Workshop
hunters_glee/
โโโ gamemodes/hunters_glee/gamemode/ # Core gamemode logic
โโโ lua/
โ โโโ glee_shopitems/ # Shop item definitions (auto-loaded)
โ โโโ glee_spawnsets/ # Enemy spawnset definitions (auto-loaded)
โ โโโ entities/ # Custom entities
โ โโโ weapons/ # Custom weapons
โ โโโ effects/ # Visual effects
โโโ materials/ # Textures and UI assets
โโโ models/ # 3D models
โโโ sound/ # Audio files
Hunter's Glee is designed to be extensible. The two main ways to add content are shop items and spawnsets.
Shop items are defined in lua/glee_shopitems/. Files are auto-loaded based on their prefix:
sh_- Shared (runs on both client and server)sv_- Server onlycl_- Client only
You can split logic between client and server. Just variables starting with sv( svOnPurchaseFunc ) have to be defined on server!
-- lua/glee_shopitems/sh_my_items.lua
local shopHelpers = GAMEMODE.shopHelpers
local items = {
["my_item_id"] = {
name = "My Item",
desc = "A description of what this item does.",
shCost = 50,
tags = { "ITEMS", "Weapon" }, -- Category tag, and misc tag (see below)
purchaseTimes = {
GAMEMODE.ROUND_INACTIVE, -- Buyable during preparation
GAMEMODE.ROUND_ACTIVE, -- Buyable during the hunt
},
shPurchaseCheck = shopHelpers.aliveCheck, -- Must be alive to buy
svOnPurchaseFunc = function( purchaser )
-- Server-side logic when purchased
purchaser:Give( "weapon_pistol" )
end,
},
}
GAMEMODE:GobbleShopItems( items )| Field | Required | Description |
|---|---|---|
name |
โ | Display name in the shop |
desc |
โ | Description (string or function) |
shCost |
โ | Cost in score (negative = gives score) |
tags |
โ | Category tags as indexed table (e.g., {"ITEMS", "Weapon"}) |
purchaseTimes |
โ | When purchasable: ROUND_INACTIVE, ROUND_ACTIVE |
svOnPurchaseFunc |
โ | Server function called on purchase: function(purchaser, itemId) |
shPurchaseCheck |
โ | Validation function(s): function(purchaser) -> bool, reason |
markup |
โ | Price multiplier during active hunt |
markupPerPurchase |
โ | Additional markup per purchase |
cooldown |
โ | Seconds between purchases (math.huge = once per round) |
weight |
โ | Sort order within category (lower = higher) |
shCanShowInShop |
โ | Visibility function: function(purchaser) -> bool |
Items appear in categories based on their first matching tag:
| Tag | Category | Visibility |
|---|---|---|
ITEMS |
Items | Alive players |
INNATE |
Innate | Alive players |
DEADSACRIFICES |
Sacrifices | Dead players |
DEADGIFTS |
Gifts | Dead players |
BANK |
Bank | All players |
Additional descriptive tags (e.g., "Weapon", "Utility") don't affect categorization.
GAMEMODE.shopHelpers provides common utilities:
shopHelpers.aliveCheck( purchaser ) -- Returns true if alive
shopHelpers.undeadCheck( purchaser ) -- Returns true if dead
shopHelpers.isCheats() -- Returns true if sv_cheats is on
shopHelpers.purchaseWeapon( purchaser, {
class = "weapon_smg1",
ammoType = "SMG1",
purchaseClips = 4, -- Clips given on first purchase
resupplyClips = 2, -- Clips given on repurchase
confirmSoundWeight = 1, -- Gun cock sound intensity
} )Spawnsets define enemy waves and game parameters. They're defined in lua/glee_spawnsets/ and auto-loaded.
-- lua/glee_spawnsets/my_spawnset.lua
local mySpawnSet = {
name = "my_spawnset", -- Unique identifier
prettyName = "My Custom Mode", -- Display name
description = "A custom enemy configuration.",
-- Use "default" to inherit base values, or "default*2" for multipliers
difficultyPerMin = "default",
waveInterval = "default",
startingBudget = "default",
maxSpawnCount = 8, -- 8 is pretty low, easy
spawns = {
{
name = "hunter", -- Unique spawn identifier
prettyName = "A Hunter", -- Display name
class = "terminator_nextbot_snail", -- Entity class to spawn
spawnType = "hunter", -- Spawn type
difficultyCost = { 10, 15 }, -- Cost range (random)
countClass = "terminator_nextbot_snail*", -- Pattern for counting (* = wildcard)
minCount = { 1 }, -- Always maintain this many
maxCount = { 5 }, -- Never exceed this many
},
},
}
table.insert( GLEE_SPAWNSETS, mySpawnSet )| Field | Required | Description |
|---|---|---|
name |
โ | Unique identifier, should match filename |
prettyName |
โ | Display name for voting/UI |
description |
โ | Description shown to players |
spawns |
โ | Array of spawn definitions |
difficultyPerMin |
โ | How fast difficulty scales |
waveInterval |
โ | Time between spawn waves, skipped if all hunters are cleared |
diffBumpWhenWaveKilled |
โ | Difficulty boost when wave cleared |
startingBudget |
โ | Initial spawn budget |
spawnCountPerDifficulty |
โ | Spawns per difficulty point |
startingSpawnCount |
โ | Initial spawn count |
maxSpawnCount |
โ | Hard cap on enemy count |
maxSpawnDist |
โ | Hard cap on the dynamically marching spawn distance |
roundStartSound |
โ | Sound on round start |
roundEndSound |
โ | Sound on round end |
genericSpawnerRate |
โ | Crate/item spawn rate multiplier |
chanceToBeVotable |
โ | Percent chance to appear in !rtm vote, 0-100, accepts float |
Values can be:
"nil"-- Use base spawnset value"default"- Explicity use base spawnset value"default*N"- Multiply base value by N{ min, max }- Random value in range- Direct number - 8, 10, 11.25, etc ( not recommended, random value in range is much more fun )
| Field | Required | Description |
|---|---|---|
name |
โ | Unique identifier for this spawn entry |
prettyName |
โ | Display name |
class |
โ | Entity class to spawn |
spawnType |
โ | Spawning algorithm type, only supports "hunter" presently |
difficultyCost |
โ | Budget cost to spawn ( number or {min, max} ) |
countClass |
โ | Class pattern for counting ( * = wildcard ) |
minCount |
โ | Minimum maintained count |
maxCount |
โ | Maximum allowed count |
hardRandomChance |
โ | { min, max } percent chance to even consider |
preSpawnedFuncs |
โ | Functions called before hunter:Spawn() : function(spawnData, npc) |
postSpawnedFuncs |
โ | Functions called after hunter:Spawn() : function(spawnData, npc) |
-- lua/glee_spawnsets/the_true_machine.lua
local function applySynthflesh( spawnData, npc )
npc:SetMaterial( "phoenix_storms/wire/pcb_red" )
end
local function announceArrival( spawnData, npc )
huntersGlee_Announce( player.GetAll(), 100, 10, "The facade is gone.\nOnly the machine remains." )
end
local trueHorror = {
name = "the_true_machine",
prettyName = "The True Machine",
description = "They've stopped pretending to be human.",
difficultyPerMin = "default*1.5",
waveInterval = "default",
startingBudget = "default",
maxSpawnCount = 6,
chanceToBeVotable = 10,
spawns = {
{
name = "synthflesh_terminator",
prettyName = "Synthflesh Terminator",
class = "terminator_nextbot_snail",
spawnType = "hunter",
difficultyCost = { 12, 18 },
countClass = "terminator_nextbot_snail*",
minCount = { 1 },
maxCount = { 6 },
postSpawnedFuncs = { applySynthflesh, announceArrival },
},
},
}
table.insert( GLEE_SPAWNSETS, trueHorror )Referenced throughout the codebase:
| Constant | Value | Description |
|---|---|---|
GAMEMODE.ROUND_INVALID |
-1 | Missing navmesh |
GAMEMODE.ROUND_SETUP |
0 | Initial setup |
GAMEMODE.ROUND_ACTIVE |
1 | Hunt in progress |
GAMEMODE.ROUND_INACTIVE |
2 | Preparation phase |
GAMEMODE.ROUND_LIMBO |
3 | Displaying winners |
See LICENSE for details.
Hunter's Glee is a passion project: a fantastic testbed for new ideas and always a great laugh. Contributions welcome!