diff --git a/.luacheckrc b/.luacheckrc index 04124a169..5f2e2a49a 100644 --- a/.luacheckrc +++ b/.luacheckrc @@ -25,10 +25,100 @@ globals = { "ACT_HL2MP_GESTURE_RELOAD_SHOTGUN", "ACT_HL2MP_GESTURE_RELOAD_SLAM", "ACT_HL2MP_GESTURE_RELOAD_SMG1", + "ACT_HL2MP_IDLE_AR2", + "ACT_HL2MP_IDLE_CAMERA", + "ACT_HL2MP_IDLE_CROSSBOW", + "ACT_HL2MP_IDLE_CROUCH_AR2", + "ACT_HL2MP_IDLE_CROUCH_CAMERA", + "ACT_HL2MP_IDLE_CROUCH_CROSSBOW", + "ACT_HL2MP_IDLE_CROUCH_DUEL", + "ACT_HL2MP_IDLE_CROUCH_FIST", + "ACT_HL2MP_IDLE_CROUCH_GRENADE", + "ACT_HL2MP_IDLE_CROUCH_KNIFE", + "ACT_HL2MP_IDLE_CROUCH_MAGIC", + "ACT_HL2MP_IDLE_CROUCH_MELEE", + "ACT_HL2MP_IDLE_CROUCH_MELEE2", + "ACT_HL2MP_IDLE_CROUCH_PASSIVE", + "ACT_HL2MP_IDLE_CROUCH_PHYSGUN", + "ACT_HL2MP_IDLE_CROUCH_PISTOL", + "ACT_HL2MP_IDLE_CROUCH_REVOLVER", + "ACT_HL2MP_IDLE_CROUCH_RPG", + "ACT_HL2MP_IDLE_CROUCH_SHOTGUN", + "ACT_HL2MP_IDLE_CROUCH_SLAM", + "ACT_HL2MP_IDLE_CROUCH_SMG1", "ACT_HL2MP_IDLE_CROUCH_ZOMBIE", + "ACT_HL2MP_IDLE_DUEL", + "ACT_HL2MP_IDLE_FIST", + "ACT_HL2MP_IDLE_GRENADE", + "ACT_HL2MP_IDLE_KNIFE", + "ACT_HL2MP_IDLE_MAGIC", + "ACT_HL2MP_IDLE_MELEE", + "ACT_HL2MP_IDLE_MELEE2", + "ACT_HL2MP_IDLE_PASSIVE", + "ACT_HL2MP_IDLE_PHYSGUN", + "ACT_HL2MP_IDLE_PISTOL", + "ACT_HL2MP_IDLE_REVOLVER", + "ACT_HL2MP_IDLE_RPG", + "ACT_HL2MP_IDLE_SHOTGUN", + "ACT_HL2MP_IDLE_SLAM", + "ACT_HL2MP_IDLE_SMG1", "ACT_HL2MP_IDLE_ZOMBIE", + "ACT_HL2MP_RUN_AR2", + "ACT_HL2MP_RUN_CAMERA", + "ACT_HL2MP_RUN_CROSSBOW", + "ACT_HL2MP_RUN_DUEL", + "ACT_HL2MP_RUN_FIST", + "ACT_HL2MP_RUN_GRENADE", + "ACT_HL2MP_RUN_KNIFE", + "ACT_HL2MP_RUN_MAGIC", + "ACT_HL2MP_RUN_MELEE", + "ACT_HL2MP_RUN_MELEE2", + "ACT_HL2MP_RUN_PASSIVE", + "ACT_HL2MP_RUN_PHYSGUN", + "ACT_HL2MP_RUN_PISTOL", + "ACT_HL2MP_RUN_REVOLVER", + "ACT_HL2MP_RUN_RPG", + "ACT_HL2MP_RUN_SHOTGUN", + "ACT_HL2MP_RUN_SLAM", + "ACT_HL2MP_RUN_SMG1", "ACT_HL2MP_RUN_ZOMBIE", + "ACT_HL2MP_WALK_AR2", + "ACT_HL2MP_WALK_CAMERA", + "ACT_HL2MP_WALK_CROSSBOW", + "ACT_HL2MP_WALK_CROUCH_AR2", + "ACT_HL2MP_WALK_CROUCH_CAMERA", + "ACT_HL2MP_WALK_CROUCH_CROSSBOW", + "ACT_HL2MP_WALK_CROUCH_DUEL", + "ACT_HL2MP_WALK_CROUCH_FIST", + "ACT_HL2MP_WALK_CROUCH_GRENADE", + "ACT_HL2MP_WALK_CROUCH_KNIFE", + "ACT_HL2MP_WALK_CROUCH_MAGIC", + "ACT_HL2MP_WALK_CROUCH_MELEE", + "ACT_HL2MP_WALK_CROUCH_MELEE2", + "ACT_HL2MP_WALK_CROUCH_PASSIVE", + "ACT_HL2MP_WALK_CROUCH_PHYSGUN", + "ACT_HL2MP_WALK_CROUCH_PISTOL", + "ACT_HL2MP_WALK_CROUCH_REVOLVER", + "ACT_HL2MP_WALK_CROUCH_RPG", + "ACT_HL2MP_WALK_CROUCH_SHOTGUN", + "ACT_HL2MP_WALK_CROUCH_SLAM", + "ACT_HL2MP_WALK_CROUCH_SMG1", "ACT_HL2MP_WALK_CROUCH_ZOMBIE_01", + "ACT_HL2MP_WALK_DUEL", + "ACT_HL2MP_WALK_FIST", + "ACT_HL2MP_WALK_GRENADE", + "ACT_HL2MP_WALK_KNIFE", + "ACT_HL2MP_WALK_MAGIC", + "ACT_HL2MP_WALK_MELEE", + "ACT_HL2MP_WALK_MELEE2", + "ACT_HL2MP_WALK_PASSIVE", + "ACT_HL2MP_WALK_PHYSGUN", + "ACT_HL2MP_WALK_PISTOL", + "ACT_HL2MP_WALK_REVOLVER", + "ACT_HL2MP_WALK_RPG", + "ACT_HL2MP_WALK_SHOTGUN", + "ACT_HL2MP_WALK_SLAM", + "ACT_HL2MP_WALK_SMG1", "ACT_HL2MP_WALK_ZOMBIE_01", "ACT_IDLE", "ACT_GMOD_GESTURE_AGREE", @@ -634,6 +724,7 @@ globals = { "CreateClientConVar", "CreateConVar", "CreateFavTable", + "CreateMaterial", "CreateSound", "CreateTransferMenu", "CurTime", @@ -788,6 +879,7 @@ globals = { "input", "list", "math", + "navmesh", "net", "os", "permissions", @@ -1035,6 +1127,7 @@ globals = { "ROLE_STARTING_HEALTH", "ROLE_STARTING_TEAM", "ROLE_STRINGS", + "ROLE_STRINGS_DEFAULT", "ROLE_STRINGS_EXT", "ROLE_STRINGS_PLURAL", "ROLE_STRINGS_RAW", diff --git a/API/GLOBAL_VARIABLES.md b/API/GLOBAL_VARIABLES.md index 974ba3a61..a5bcad811 100644 --- a/API/GLOBAL_VARIABLES.md +++ b/API/GLOBAL_VARIABLES.md @@ -146,6 +146,11 @@ Table of title-case names for each role.\ *Realm:* Client and Server\ *Added in:* 1.0.0 +### ROLE_STRINGS_DEFAULT +Table of title-case names for each role. Copy of `ROLE_STRINGS` from before role strings are changed by ConVars.\ +*Realm:* Client and Server\ +*Added in:* 2.4.4 + ### ROLE_STRINGS_EXT Table of extended (e.g. prefixed by an article) names for each role.\ *Realm:* Client and Server\ diff --git a/CONVARS.md b/CONVARS.md index b8d887dda..24d58d587 100644 --- a/CONVARS.md +++ b/CONVARS.md @@ -764,6 +764,12 @@ ttt_cannibal_eat_cooldown 10 // The amount of time (in ttt_cannibal_damage_penalty 0 // The fraction a Cannibal's damage will be scaled by when they are attacking (Only applies if ttt_cannibal_is_independent is enabled) ttt_cannibal_can_see_jesters 0 // Whether jesters are revealed (via head icons, color/icon on the scoreboard, etc.) to the Cannibal (Only applies if ttt_cannibal_is_independent is enabled) ttt_cannibal_update_scoreboard 0 // Whether the Cannibal shows dead players as missing in action (Only applies if ttt_cannibal_is_independent is enabled) +ttt_cannibal_gains_health 0 // Whether the Cannibal gains their victim's health when eating them +ttt_cannibal_gained_health_percentage 15 // What percentage of their victim's health the Cannibal gains. Set to 0 to always gain a flat 100HP (Only applies if ttt_cannibal_gains_health is enabled) +ttt_cannibal_digestion 0 // Whether the Cannibal digests and permanently kills their victims over time +ttt_cannibal_digestion_time 30 // How long in seconds a victim takes to be digested when eaten. Set to 0 for immediate digestion (Only applies if ttt_cannibal_digestion is enabled) +ttt_cannibal_digestion_poop 1 // Whether the Cannibal drops poop when a victim is digested (Only applies if ttt_cannibal_digestion is enabled) +ttt_cannibal_digestion_poop_sound 1 // Whether the Cannibal causes a sound when poop is dropped from a digested victim (Only applies if ttt_cannibal_digestion is enabled) // ---------------------------------------- diff --git a/CREATE_YOUR_OWN_ROLE.md b/CREATE_YOUR_OWN_ROLE.md index 7dcbfddfb..8655a50e4 100644 --- a/CREATE_YOUR_OWN_ROLE.md +++ b/CREATE_YOUR_OWN_ROLE.md @@ -56,7 +56,7 @@ In order to create your own role you will need to make sure you have downloaded - **.lua** - This can be done in Notepad in a pinch but at the very least we would recommend [Notepad++](https://notepad-plus-plus.org/). - **.vmt and .vtf** - [VTFEdit Reloaded](https://github.com/Sky-rym/VTFEdit-Reloaded/releases) is the best way to edit these files but if you know what you are doing there are plugins for other apps. -In this guide we will be walking through how we made the Summoner role and you can download all the templates we are using [here](/templates/role). +In this guide we will be walking through how we made the Summoner role and you can download all the templates we are using [here](/templates/role) using a tool that allows downloading directories from a GitHub repo like [this one](https://kinolien.github.io/gitzip/) *(Note: In most cases you won't need to enter a token despite its recommendation)*. Last thing to do before you are ready to get started is to unzip that file which should give you 4 .psd files and a folder like this: @@ -81,7 +81,7 @@ ROLE.desc = [[]] ROLE.shortdesc = "" -ROLE.team = +ROLE.team = ROLE_TEAM_ ROLE.shop = nil ROLE.loadout = {} @@ -835,7 +835,9 @@ end ### Role Registration -The next line simply tells CR for TTT to register your role and passes through all the relevant information. You do not need to edit this line. CR for TTT automatically defines an enumeration for your role, `ROLE_%NAMERAW%` as well as helper functions `Get%NAMERAW%`, `Is%NAMERAW%` and `IsActive%NAMERAW%` if you would like to use them to add extra logic for your role. +The next line simply tells CR for TTT to register your role and passes through all the relevant information. You do not need to edit this line. CR for TTT automatically defines an enumeration for your role, `ROLE_%NAMERAW%` as well as helper functions `Get%NAME%`, `Is%NAME%` and `IsActive%NAME%` if you would like to use them to add extra logic for your role. + +*(Note: All spaces are removed from `%NAME%` before `Get%NAME%`, `Is%NAME%` and `IsActive%NAME%` are created)* ### Final Block diff --git a/README.md b/README.md index d84a89472..291f9d02a 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,16 @@ If you would like to test the available configurations, we recommend using ULX/U - MaxCooljazz from the CR4TTT Community for allowing us to integrate their Spy role into the mod. - [Game icons](https://game-icons.net), [Noun Project](https://thenounproject.com), and [Icons8](https://icons8.com) for the role icons. - [Videvo](https://www.videvo.net/profile/videvo/) for the royalty-free [extinguish sound](https://www.videvo.net/sound-effect/short-light-fire-exti-pe363704/255924/) and cough sounds: [1](https://www.videvo.net/sound-effect/human-cough-33/427996/), [2](https://www.videvo.net/sound-effect/human-cough-36/427999/), [3](https://www.videvo.net/sound-effect/human-cough-39/428002/), [4](https://www.videvo.net/sound-effect/human-cough-63/428026/) +- [freesound.org](https://freesound.org/) for the poop sounds used by the Cannibal's digestion effect + - Licensed under [Creative Commons 0](https://creativecommons.org/public-domain/cc0/) + - [fart](https://freesound.org/people/MacKaffee/sounds/326143/) by [MacKaffee](https://freesound.org/people/MacKaffee/) + - [Short, definite fart](https://freesound.org/people/ycbcr/sounds/249583/) by [ycbcr](https://freesound.org/people/ycbcr/) + - [small fart](https://freesound.org/people/KataVlogsYT/sounds/324453/) by [KataVlogsYT](https://freesound.org/people/KataVlogsYT/) + - [queef_fart](https://freesound.org/people/faen/sounds/167090/) by [faen](https://freesound.org/people/faen/) + - [WAV Fart Vegan 047](https://freesound.org/people/frenkfurth/sounds/650693/) by [frenkfurth](https://freesound.org/people/frenkfurth/) + - Licensed under [Creative Commons BY 4.0](https://creativecommons.org/licenses/by/4.0/) + - [fart_anxious](https://freesound.org/people/IFartInUrGeneralDirection/sounds/37850/) by [IFartInUrGeneralDirection](https://freesound.org/people/IFartInUrGeneralDirection/) +- [Deika](https://steamcommunity.com/id/deikaeus) for the [model](https://steamcommunity.com/sharedfiles/filedetails/?id=304354340) used by the Cannibal's digestion poop effect - Our friends and everyone on the Discord server for their suggestions and help testing. ## Conflicts diff --git a/RELEASE.md b/RELEASE.md index bee2ccbc5..4a19fbbcc 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,5 +1,31 @@ # Release Notes +## 2.4.4 (Beta) +**Released: March 21st, 2026** + +### Additions +- Added messages to existing twins when a player becomes a twin during an active round +- Added option to allow Cannibal to gain a configurable percentage of their victims' health (disabled by default) (Thanks Joel!) +- Added option to allow Cannibal to "digest" (permanently kill) their victims a configurable amount of time after eating them (disabled by default) (Thanks Joel!) +- Added option to allow Cannibal to drop a poop when a victim is fully digested, with or without an audible cue (enabled by default, but depends on digestion being enabled) (Thanks Joel!) + +### Changes +- Changed role pack to apply next round, if the round is already active +- Changed logic to reassign a player's role if they change roles into being a solo twin during an active round +- Ported "TTT: Use gmod_language setting by default" +- Ported "TTT: Translatability improvements" +- Ported "TTT: Equipment menu sorting options" + +### Fixes +- Fixed cheat sheet getting cut off when it exceeded the height of the screen and a role pack was enabled +- Fixed jester, detective, innocent, and traitor role rename ConVars not working +- Fixed error in Cannibal's role weapon HUD when weapon was selected from last round (Thanks Stig!) +- Fixed detective hats not hiding when the owning player was eaten by the Cannibal +- Ported "Add check for checking weapon GetHeadshotMultiplier" + +### Developer +- Added `ROLE_STRINGS_DEFAULT` as a copy of `ROLE_STRINGS` from before role strings are changed by ConVars + ## 2.4.3 (Beta) **Released: February 21st, 2026** diff --git a/docs/api/global_variables.html b/docs/api/global_variables.html index 9bacb1dda..1fb197e80 100644 --- a/docs/api/global_variables.html +++ b/docs/api/global_variables.html @@ -220,6 +220,13 @@

ROLE_STRINGS

Added in: 1.0.0

+

ROLE_STRINGS_DEFAULT  

+

+ Table of title-case names for each role. Copy of ROLE_STRINGS from before role strings are changed by ConVars.
+ Realm: Client and Server
+ Added in: 2.4.4 +

+

ROLE_STRINGS_EXT

Table of extended (e.g. prefixed by an article) names for each role.
diff --git a/docs/teams/jester.html b/docs/teams/jester.html index 4ffd23c2c..a8123bb40 100644 --- a/docs/teams/jester.html +++ b/docs/teams/jester.html @@ -580,7 +580,25 @@

Role Configuration:

ttt_cannibal_damage_penalty 0 Float - The fraction a Cannibal's damage will be scaled by when they are attacking. (Requires ttt_bodysnatcher_is_independent to be enabled.) + The fraction a Cannibal's damage will be scaled by when they are attacking. (Requires ttt_cannibal_is_independent to be enabled.) + + + ttt_cannibal_digestion   + 0 + Boolean + Whether the Cannibal digests and permanently kills their victims over time. + + + ttt_cannibal_digestion_poop   + 1 + Boolean + Whether the Cannibal drops poop when a victim is digested. (Requires ttt_cannibal_digestion to be enabled.) + + + ttt_cannibal_digestion_poop_sound   + 1 + Boolean + Whether the Cannibal causes a sound when poop is dropped from a digested victim. (Requires ttt_cannibal_digestion to be enabled.) ttt_cannibal_eat_cooldown @@ -588,6 +606,18 @@

Role Configuration:

Integer The amount of time in seconds between uses of the Cannibal's Cannibalizer. + + ttt_cannibal_gains_health   + 0 + Boolean + Whether the Cannibal gains their victim's health when eating them. + + + ttt_cannibal_gained_health_percentage   + 15 + Integer + What percentage of their victim's health the Cannibal gains. Set to 0 to always gain a flat 100HP (Requires ttt_cannibal_gains_health to be enabled.) + ttt_cannibal_is_independent 0 @@ -598,7 +628,7 @@

Role Configuration:

ttt_cannibal_update_scoreboard 0 Boolean - Whether the Cannibal shows dead players as missing in action. (Requires ttt_bodysnatcher_is_independent to be enabled.) + Whether the Cannibal shows dead players as missing in action. (Requires ttt_cannibal_is_independent to be enabled.) diff --git a/docs/tutorials/create_your_own_role.html b/docs/tutorials/create_your_own_role.html index 611b4806b..fc2a83c4e 100644 --- a/docs/tutorials/create_your_own_role.html +++ b/docs/tutorials/create_your_own_role.html @@ -94,7 +94,7 @@

Before You Start

  • .lua - This can be done in Notepad in a pinch but at the very least we would recommend Notepad++.
  • .vmt and .vtf - VTFEdit Reloaded is the best way to edit these files but if you know what you are doing there are plugins for other apps.
  • -

    In this guide we will be walking through how we made the Summoner role and you can download all the templates we are using here.

    +

    In this guide we will be walking through how we made the Summoner role and you can download all the templates we are using here using a tool that allows downloading directories from a GitHub repo like this one (Note: In most cases you won't need to enter a token despite its recommendation).

    Last thing to do before you are ready to get started is to unzip that file which should give you 4 .psd files and a folder like this:

    @@ -937,7 +937,7 @@

    Tutorial Page

    end) end -

    (Note: If you would like to make this information translatable, see the Translations section of this document. )

    +

    (Note: If you would like to make this information translatable, see the Translations section of this document.)

    For a more complex example, lets take the same string from before but change the phrase "traitor team" to be the color of the traitor team in TTT. To do that, we're going to use some fairly basic HTML instead of just raw text:

    if CLIENT then
         hook.Add("TTTTutorialRoleText", "SummonerTutorialRoleText", function(role, titleLabel, roleIcon)
    @@ -949,7 +949,9 @@ 

    Tutorial Page

    end

    Role Registration

    -

    The next line simply tells CR for TTT to register your role and passes through all the relevant information. You do not need to edit this line. CR for TTT automatically defines an enumeration for your role, ROLE_%NAMERAW% as well as helper functions Get%NAMERAW%, Is%NAMERAW% and IsActive%NAMERAW% if you would like to use them to add extra logic for your role.

    +

    The next line simply tells CR for TTT to register your role and passes through all the relevant information. You do not need to edit this line. CR for TTT automatically defines an enumeration for your role, ROLE_%NAMERAW% as well as helper functions Get%NAME%, Is%NAME% and IsActive%NAME% if you would like to use them to add extra logic for your role.

    + +

    (Note: All spaces are removed from %NAME% before Get%NAME%, Is%NAME% and IsActive%NAME% are created)

    Final Block

    Finally we have this block of code:

    diff --git a/gamemodes/terrortown/content/materials/models/poo/poo.vmt b/gamemodes/terrortown/content/materials/models/poo/poo.vmt new file mode 100644 index 000000000..91d40117a --- /dev/null +++ b/gamemodes/terrortown/content/materials/models/poo/poo.vmt @@ -0,0 +1,10 @@ +"VertexLitGeneric" +{ + "$baseTexture" "Models/poo/poo" + "$bumpmap" "Models/poo/poo_n" + // -- From here down is new stuff which will only be applied if $phong is set to 1 -- + "$phong" "1" + "$phongboost" "4" + "$phongexponent" "30" + "$phongfresnelranges" "[.2 .5 1]" +} diff --git a/gamemodes/terrortown/content/materials/models/poo/poo.vtf b/gamemodes/terrortown/content/materials/models/poo/poo.vtf new file mode 100644 index 000000000..f32c30dc3 Binary files /dev/null and b/gamemodes/terrortown/content/materials/models/poo/poo.vtf differ diff --git a/gamemodes/terrortown/content/materials/models/poo/poo_n.vtf b/gamemodes/terrortown/content/materials/models/poo/poo_n.vtf new file mode 100644 index 000000000..3a9d0b58f Binary files /dev/null and b/gamemodes/terrortown/content/materials/models/poo/poo_n.vtf differ diff --git a/gamemodes/terrortown/content/models/poo/poo.dx80.vtx b/gamemodes/terrortown/content/models/poo/poo.dx80.vtx new file mode 100644 index 000000000..3182d23c3 Binary files /dev/null and b/gamemodes/terrortown/content/models/poo/poo.dx80.vtx differ diff --git a/gamemodes/terrortown/content/models/poo/poo.dx90.vtx b/gamemodes/terrortown/content/models/poo/poo.dx90.vtx new file mode 100644 index 000000000..8c3f0539e Binary files /dev/null and b/gamemodes/terrortown/content/models/poo/poo.dx90.vtx differ diff --git a/gamemodes/terrortown/content/models/poo/poo.mdl b/gamemodes/terrortown/content/models/poo/poo.mdl new file mode 100644 index 000000000..6cb10c73c Binary files /dev/null and b/gamemodes/terrortown/content/models/poo/poo.mdl differ diff --git a/gamemodes/terrortown/content/models/poo/poo.phy b/gamemodes/terrortown/content/models/poo/poo.phy new file mode 100644 index 000000000..f2b486025 Binary files /dev/null and b/gamemodes/terrortown/content/models/poo/poo.phy differ diff --git a/gamemodes/terrortown/content/models/poo/poo.sw.vtx b/gamemodes/terrortown/content/models/poo/poo.sw.vtx new file mode 100644 index 000000000..df55c7972 Binary files /dev/null and b/gamemodes/terrortown/content/models/poo/poo.sw.vtx differ diff --git a/gamemodes/terrortown/content/models/poo/poo.vvd b/gamemodes/terrortown/content/models/poo/poo.vvd new file mode 100644 index 000000000..902257872 Binary files /dev/null and b/gamemodes/terrortown/content/models/poo/poo.vvd differ diff --git a/gamemodes/terrortown/content/sound/cannibal/poop1.wav b/gamemodes/terrortown/content/sound/cannibal/poop1.wav new file mode 100644 index 000000000..770aee196 Binary files /dev/null and b/gamemodes/terrortown/content/sound/cannibal/poop1.wav differ diff --git a/gamemodes/terrortown/content/sound/cannibal/poop2.wav b/gamemodes/terrortown/content/sound/cannibal/poop2.wav new file mode 100644 index 000000000..3adc03fee Binary files /dev/null and b/gamemodes/terrortown/content/sound/cannibal/poop2.wav differ diff --git a/gamemodes/terrortown/content/sound/cannibal/poop3.wav b/gamemodes/terrortown/content/sound/cannibal/poop3.wav new file mode 100644 index 000000000..fde256fa8 Binary files /dev/null and b/gamemodes/terrortown/content/sound/cannibal/poop3.wav differ diff --git a/gamemodes/terrortown/content/sound/cannibal/poop4.wav b/gamemodes/terrortown/content/sound/cannibal/poop4.wav new file mode 100644 index 000000000..de94b9400 Binary files /dev/null and b/gamemodes/terrortown/content/sound/cannibal/poop4.wav differ diff --git a/gamemodes/terrortown/content/sound/cannibal/poop5.wav b/gamemodes/terrortown/content/sound/cannibal/poop5.wav new file mode 100644 index 000000000..683fac570 Binary files /dev/null and b/gamemodes/terrortown/content/sound/cannibal/poop5.wav differ diff --git a/gamemodes/terrortown/content/sound/cannibal/poop6.wav b/gamemodes/terrortown/content/sound/cannibal/poop6.wav new file mode 100644 index 000000000..ca0d00158 Binary files /dev/null and b/gamemodes/terrortown/content/sound/cannibal/poop6.wav differ diff --git a/gamemodes/terrortown/entities/weapons/weapon_can_eater.lua b/gamemodes/terrortown/entities/weapons/weapon_can_eater.lua index c4272d36e..b9705de13 100644 --- a/gamemodes/terrortown/entities/weapons/weapon_can_eater.lua +++ b/gamemodes/terrortown/entities/weapons/weapon_can_eater.lua @@ -37,6 +37,15 @@ SWEP.InLoadoutFor = {ROLE_CANNIBAL} SWEP.InLoadoutForDefault = {ROLE_CANNIBAL} SWEP.DeviceCooldownConVar = CreateConVar("ttt_cannibal_eat_cooldown", "10", FCVAR_REPLICATED, "The amount of time (in seconds) between uses of the Cannibal's Cannibalizer", 0, 60) +SWEP.GainsHealthConVar = CreateConVar("ttt_cannibal_gains_health", "0", FCVAR_REPLICATED, "Whether the Cannibal gains their victim's health when eating them", 0, 1) +SWEP.GainedHealthPercentageConVar = CreateConVar("ttt_cannibal_gained_health_percentage", "15", FCVAR_REPLICATED, "What percentage of their victim's health the Cannibal gains (set to 0 to always gain a flat 100HP)", 0, 500) +SWEP.DigestionConVar = CreateConVar("ttt_cannibal_digestion", "0", FCVAR_REPLICATED, "Whether the Cannibal digests and permanently kills their victims over time", 0, 1) +SWEP.DigestionTimeConVar = CreateConVar("ttt_cannibal_digestion_time", "30", FCVAR_REPLICATED, "How long in seconds a victim takes to be digested when eaten (set to 0 for immediate digestion)", 0, 300) + +if SERVER then + SWEP.DigestionPoopConVar = CreateConVar("ttt_cannibal_digestion_poop", "1", FCVAR_NONE, "Whether the Cannibal drops poop when a victim is digested", 0, 1) + SWEP.DigestionPoopSoundConVar = CreateConVar("ttt_cannibal_digestion_poop_sound", "1", FCVAR_NONE, "Whether the Cannibal causes a sound when poop is dropped from a digested victim.", 0, 1) +end local eatSounds = { "cannibal/eat1.wav", @@ -44,6 +53,15 @@ local eatSounds = { "cannibal/eat3.wav" } +local poopSounds = { + "cannibal/poop1.wav", + "cannibal/poop2.wav", + "cannibal/poop3.wav", + "cannibal/poop4.wav", + "cannibal/poop5.wav", + "cannibal/poop6.wav" +} + function SWEP:Initialize() if CLIENT then self:AddHUDHelp("can_eater_help_pri", nil, true) @@ -62,6 +80,14 @@ function SWEP:OnDrop() self:Remove() end +function SWEP:OnRemove() + if SERVER then + for sid64, _ in pairs(CANNIBAL.playerWeapons) do + timer.Remove("TTTCannibalDigestion_" .. sid64) + end + end +end + function SWEP:Deploy() if SERVER and IsValid(self:GetOwner()) then self:GetOwner():DrawViewModel(false) @@ -98,6 +124,9 @@ function SWEP:PrimaryAttack() hitEnt:SpectateEntity(owner) hitEnt:DrawViewModel(false) hitEnt:DrawWorldModel(false) + if IsValid(hitEnt.hat) then + hitEnt.hat:SetNoDraw(true) + end local sID64 = hitEnt:SteamID64() @@ -125,6 +154,73 @@ function SWEP:PrimaryAttack() if cooldown > 0 then self:SetDeviceCooldownEnd(CurTime() + cooldown) end + + -- Cannibal health gain + if self.GainsHealthConVar:GetBool() then + local gained_health_percentage = self.GainedHealthPercentageConVar:GetInt() + local victimHealth = hitEnt:Health() + local cannibalHealth = owner:Health() + + local gainedHealth + if gained_health_percentage == 0 then + gainedHealth = 100 + else + gainedHealth = math.floor((gained_health_percentage / 100) * victimHealth) + end + + if gainedHealth > 0 then + owner:SetHealth(cannibalHealth + gainedHealth) + end + end + + -- Victim digestion + if self.DigestionConVar:GetBool() then + local digestion_time = self.DigestionTimeConVar:GetInt() + -- Ensure there's a short delay to allow time for the vars to be set first + if digestion_time == 0 then + digestion_time = 0.1 + end + + timer.Create("TTTCannibalDigestion_" .. sID64, digestion_time, 1, function() + if not IsPlayer(hitEnt) then return end + if not IsPlayer(owner) then return end + + -- Only digest if they are still in THIS cannibal's tummy + if hitEnt.TTTCannibalEaten ~= owner:SteamID64() then return end + + hitEnt:Kill() + hitEnt:ClearProperty("TTTCannibalEaten") + + hitEnt:SetParent(nil) + hitEnt:SpectateEntity(nil) + + hitEnt:QueueMessage(MSG_PRINTBOTH, "You have been fully digested!") + owner:QueueMessage(MSG_PRINTBOTH, "You have fully digested " .. hitEnt:Nick() .. "!") + + -- Spawn poop at cannibal's position + if self.DigestionPoopConVar:GetBool() then + local poop = ents.Create("prop_physics") + if IsValid(poop) then + local fingerprints = { owner } + poop:SetModel("models/poo/poo.mdl") + + local forward = owner:GetForward() + local dropPos = owner:GetPos() + forward * -30 + Vector(0, 0, 10) + poop:SetPos(dropPos) + + poop:SetAngles(Angle(0, math.random(0, 360), 0)) + poop:Spawn() + poop:Activate() + poop:SetCollisionGroup(COLLISION_GROUP_WEAPON) + poop.fingerprints = fingerprints + + if self.DigestionPoopSoundConVar:GetBool()then + owner:EmitSound(poopSounds[math.random(#poopSounds)], 100) + end + end + end + end) + end end end @@ -152,7 +248,7 @@ if CLIENT then local cooldownLeft = self:GetDeviceCooldownEnd() - CurTime() local progress = 1 - (cooldownLeft / cooldown) - if cooldownLeft > -3 or self.DeployTime > CurTime() - 3 then + if cooldownLeft > -3 or (self.DeployTime and self.DeployTime > CurTime() - 3) then if progress > 1 then CRHUD:PaintProgressBar(x, y, w, Color(0, 255, 0, 155), "READY TO EAT", 1) else @@ -160,4 +256,4 @@ if CLIENT then end end end -end \ No newline at end of file +end diff --git a/gamemodes/terrortown/gamemode/cl_cheatsheet.lua b/gamemodes/terrortown/gamemode/cl_cheatsheet.lua index ea800bf2f..14d6ff304 100644 --- a/gamemodes/terrortown/gamemode/cl_cheatsheet.lua +++ b/gamemodes/terrortown/gamemode/cl_cheatsheet.lua @@ -136,12 +136,9 @@ hook.Add("PlayerButtonDown", "CheatSheet_PlayerButtonDown", function(ply, button local scrollbarWidth = 15 local m = 5 - local packName = GetConVar("ttt_role_pack"):GetString() + local packName = ROLEPACKS.GetCurrentRolePackName() local packDesc - if #packName == 0 then - rolePackHeight = 0 - rolePackDescHeight = 0 - elseif ROLE_PACK_DETAILS then + if #packName > 0 then local displayName = ROLE_PACK_DETAILS.displayName if displayName and #displayName > 0 then packName = displayName @@ -152,6 +149,9 @@ hook.Add("PlayerButtonDown", "CheatSheet_PlayerButtonDown", function(ply, button packDesc = nil rolePackDescHeight = 0 end + else + rolePackHeight = 0 + rolePackDescHeight = 0 end local w, h, detectivesHeight, innocentsHeight, traitorsHeight, jestersHeight, independentsHeight, monstersHeight @@ -244,7 +244,7 @@ hook.Add("PlayerButtonDown", "CheatSheet_PlayerButtonDown", function(ply, button end local dlist = vgui.Create("DScrollPanel", dbackground) - dlist:SetSize(w, h) + dlist:SetSize(w, h - (rolePackHeight + rolePackDescHeight)) dlist:SetPos(0, rolePackHeight + rolePackDescHeight) local dcanvas = dlist:GetCanvas() diff --git a/gamemodes/terrortown/gamemode/cl_equip.lua b/gamemodes/terrortown/gamemode/cl_equip.lua index 5b1fdce2c..b33037055 100644 --- a/gamemodes/terrortown/gamemode/cl_equip.lua +++ b/gamemodes/terrortown/gamemode/cl_equip.lua @@ -19,6 +19,7 @@ local RunHook = hook.Run local GetWeapon = weapons.GetStored local GetTranslation = LANG.GetTranslation local GetPTranslation = LANG.GetParamTranslation +local MathFloor = math.floor local MathRandom = math.random local StringFind = string.find local StringLower = string.lower @@ -29,6 +30,7 @@ local TableMerge = table.Merge local TableRemove = table.remove local TableShuffle = table.Shuffle local TableSort = table.sort +local SafeTranslate = LANG.TryTranslation -- BEM client convars and config menu local numColsVar = CreateClientConVar("ttt_bem_cols", 4, true, false, "Sets the number of columns in the Traitor/Detective menu's item list.") @@ -38,8 +40,10 @@ local showCustomVar = CreateClientConVar("ttt_bem_marker_custom", 1, true, false local showFavoriteVar = CreateClientConVar("ttt_bem_marker_fav", 1, true, false, "Should favorite items get a marker?") local showSlotVar = CreateClientConVar("ttt_bem_marker_slot", 1, true, false, "Should items get a slot-marker?") local showLoadoutEquipment = CreateClientConVar("ttt_show_loadout_equipment", 0, true, false, "Should loadout equipment show in shops?") -local sortAlphabetically = CreateClientConVar("ttt_sort_alphabetically", 1, true, false, "Should the shop sort alphabetically?") -local sortBySlotFirst = CreateClientConVar("ttt_sort_by_slot_first", 0, true, false, "Should the shop sort by slot first?") + +local equipment_sorting = CreateClientConVar("ttt_equipment_sorting", "default", true) +local equipment_ascending = CreateClientConVar("ttt_equipment_ascending", "1", true) +local equipment_hide_unbuyable = CreateClientConVar("ttt_equipment_hide_unbuyable", "0", true) hook.Add("Initialize", "EquipmentMenu_Initialize", function() LANG.AddToLanguage("english", "set_title_equipment", "Equipment/Shop settings") @@ -48,8 +52,6 @@ hook.Add("Initialize", "EquipmentMenu_Initialize", function() LANG.AddToLanguage("english", "set_equipment_convar_custom", "Show custom item marker") LANG.AddToLanguage("english", "set_equipment_convar_fav", "Show favourite item marker") LANG.AddToLanguage("english", "set_equipment_convar_loadout", "Show loadout items") - LANG.AddToLanguage("english", "set_equipment_convar_alpha", "Sort alphabetically") - LANG.AddToLanguage("english", "set_equipment_convar_sort_by_slot", "Sort by slot first") end) hook.Add("TTTSettingsConfigTabSections", "EquipmentMenu_TTTSettingsConfigTabSections", function(dsettings) @@ -72,8 +74,7 @@ hook.Add("TTTSettingsConfigTabSections", "EquipmentMenu_TTTSettingsConfigTabSect dbemsettings:CheckBox(GetTranslation("set_equipment_convar_custom"), "ttt_bem_marker_custom") dbemsettings:CheckBox(GetTranslation("set_equipment_convar_fav"), "ttt_bem_marker_fav") dbemsettings:CheckBox(GetTranslation("set_equipment_convar_loadout"), "ttt_show_loadout_equipment") - dbemsettings:CheckBox(GetTranslation("set_equipment_convar_alpha"), "ttt_sort_alphabetically") - dbemsettings:CheckBox(GetTranslation("set_equipment_convar_sort_by_slot"), "ttt_sort_by_slot_first") + dbemsettings:CheckBox(GetTranslation("set_hide_unbuyable"), "ttt_equipment_hide_unbuyable") CallHook("TTTSettingsConfigTabFields", nil, "BEM", dbemsettings) @@ -142,7 +143,7 @@ local function ItemIsWeapon(item) return not tonumber(item.id) end function GetEquipmentForRole(role, promoted, block_randomization, block_exclusion, ignore_cache, rolepack_weps) WEPS.PrepWeaponsLists(role) - local packName = GetConVar("ttt_role_pack"):GetString() + local packName = ROLEPACKS.GetCurrentRolePackName() if rolepack_weps == nil and #packName > 0 then rolepack_weps = {Buyables = WEPS.RolePackBuyableWeapons[role], Excludes = WEPS.RolePackExcludeWeapons[role], NoRandoms = WEPS.RolePackBypassRandomWeapons[role]} elseif rolepack_weps == false or #packName == 0 then @@ -249,7 +250,7 @@ function GetEquipmentForRole(role, promoted, block_randomization, block_exclusio end -- If we have sync roles and this role is one of them, save the item info for later - if sync_roles and table.HasValue(sync_roles, r) then + if sync_roles and TableHasValue(sync_roles, r) then TableInsert(sync_equipment, i) end end @@ -468,7 +469,7 @@ end local color_bad = Color(220, 60, 60, 255) local color_good = Color(255, 255, 255, 255) --- Creates tabel of labels showing the status of ordering prerequisites +-- Creates table of labels showing the status of ordering prerequisites local function PreqLabels(parent, x, y) local tbl = {} @@ -584,8 +585,6 @@ function PANEL:SelectPanel(pnl) end vgui.Register("EquipSelect", PANEL, "DPanelSelect") -local SafeTranslate = LANG.TryTranslation - local color_darkened = Color(255, 255, 255, 80) -- BEM helper functions @@ -643,6 +642,161 @@ hook.Add("OnPauseMenuShow", "EquipMenu_OnPauseMenuShow", function() end end) +local ListPanel = nil +local function CanOrder(item, owned_ids) + -- not orderable + if not ListPanel.update_preqs(item) then return false end + + local ply = LocalPlayer() + if ply:GetCredits() <= 0 then return false end + + -- already owned + if TableHasValue(owned_ids, item.id) then + return false + end + + if ItemIsWeapon(item) then + -- already carrying a weapon for this slot + if not CanCarryWeapon(item) then return false end + elseif ply:HasEquipmentItem(tonumber(item.id)) then + return false + end + + -- already bought the item before + if item.limited and ply:HasBought(tostring(item.id)) then return false end + -- doesn't have the required items + if not WEPS.PlayerOwnsWepReqs(ply, item) then return false end + + return true +end + +local function GetOwnedEquipment() + -- Determine if we already have equipment + local owned_ids = {} + for _, wep in ipairs(LocalPlayer():GetWeapons()) do + if IsValid(wep) and wep:IsEquipment() then + TableInsert(owned_ids, wep:GetClass()) + end + end + + -- Stick to one value for no equipment + if #owned_ids == 0 then + owned_ids = nil + end + + return owned_ids +end + +local sort_funcs = { + default = { + name = "equip_sort_default", + func = function(a, b) + local aItem, bItem = not ItemIsWeapon(a), not ItemIsWeapon(b) + + if aItem or bItem then + -- sort items by id + if aItem and bItem then + return a.id < b.id + end + + -- keep items above weapons + return aItem + end + + return StringLower(SafeTranslate(a.name)) < StringLower(SafeTranslate(b.name)) + end + }, + name = { + name = "equip_spec_name", + func = function(a, b) + return StringLower(SafeTranslate(a.name)) < StringLower(SafeTranslate(b.name)) + end + }, + slot = { + name = "equip_sort_slot", + func = function(a, b) + local aSlot = a.slot or 0 + local bSlot = b.slot or 0 + + -- sort items by id + if aSlot == 0 and bSlot == 0 then + return a.id < b.id + end + + if aSlot == bSlot then + return StringLower(SafeTranslate(a.name)) < StringLower(SafeTranslate(b.name)) + end + + return aSlot < bSlot + end + } +} + +local function SortEquipmentPanels(pnls) + local sort = equipment_sorting:GetString() or "default" + local sort_func = sort_funcs[sort].func + + local ascending = equipment_ascending:GetBool() + local hide_unbuyable = equipment_hide_unbuyable:GetBool() + + local owned_ids = hide_unbuyable and GetOwnedEquipment() + + TableSort(pnls, function(a, b) + local aItem, bItem = a.item, b.item + + if hide_unbuyable then + local aBuyable, bBuyable = CanOrder(aItem, owned_ids), CanOrder(bItem, owned_ids) + + if aBuyable ~= bBuyable then + return aBuyable + end + end + + local ret = sort_func(aItem, bItem) + + -- if table.sort is comparing an item to itself, don't mess with the result otherwise weird stuff happens + if not ascending and aItem.id ~= bItem.id then + ret = not ret + end + + return ret + end) +end + +local function AddSortedPanels(panels) + if GetConVar("ttt_shop_random_position"):GetBool() then + panels = TableShuffle(panels) + else + SortEquipmentPanels(panels) + end + for _, panel in pairs(panels) do + ListPanel:AddPanel(panel) + end +end + +local function ReSortEquipment() + if not IsValid(ListPanel) then return end + + -- temp table for sorting + local paneltablefav = {} + local paneltable = {} + for _, ic in ipairs(ListPanel:GetItems()) do + if ic.favorite then + TableInsert(paneltablefav, ic) + else + TableInsert(paneltable, ic) + end + end + + ListPanel:Clear() + AddSortedPanels(paneltablefav) + AddSortedPanels(paneltable) + ListPanel:InvalidateLayout() +end +hook.Add("TTTLanguageChanged", "TTT_ReSortEquipment", ReSortEquipment) +cvars.AddChangeCallback("ttt_equipment_sorting", ReSortEquipment) +cvars.AddChangeCallback("ttt_equipment_ascending", ReSortEquipment) + local function DoesValueMatch(item, data, value) if not item[data] then return false end @@ -721,43 +875,69 @@ local function TraitorMenuPopup() dequip:SetPaintBackground(false) dequip:StretchToParent(padding, padding, padding, padding) - -- Determine if we already have equipment - local owned_ids = {} - for _, wep in ipairs(ply:GetWeapons()) do - if IsValid(wep) and wep.IsEquipment and wep:IsEquipment() then - TableInsert(owned_ids, wep:GetClass()) - end - end - - -- Stick to one value for no equipment - if #owned_ids == 0 then - owned_ids = nil - end - local dsearchheight = 25 local dsearchpadding = 5 + local dsortdirsize = 16 + local sw = MathFloor((dlistw - dsearchpadding) / 2) + local dsearch = vgui.Create("DTextEntry", dequip) dsearch:SetPos(0, 0) - dsearch:SetSize(dlistw, dsearchheight) + dsearch:SetSize(sw, dsearchheight) dsearch:SetPlaceholderText("Search...") dsearch:SetUpdateOnType(true) dsearch.OnGetFocus = function() dframe:SetKeyboardInputEnabled(true) end dsearch.OnLoseFocus = function() dframe:SetKeyboardInputEnabled(false) end + local dsort = vgui.Create("DPanel", dequip) + dsort:SetPos(sw + dsearchpadding, 0) + dsort:SetSize(sw, dsearchheight) + dsort:SetPaintBackground(false) + + local dsortlbl = vgui.Create("DLabel", dsort) + dsortlbl:SetFont("DermaDefaultBold") + dsortlbl:SetText(GetTranslation("sb_sortby")) + dsortlbl:SizeToContentsX() + dsortlbl:SetTall(dsearchheight) + dsortlbl:SetColor(COLOR_WHITE) + + local dsorttype = vgui.Create("DComboBox", dsort) + dsorttype:MoveRightOf(dsortlbl, m) + dsorttype:SetSize(dsort:GetWide() - dsortlbl:GetWide() - m - dsortdirsize, dsearchheight) + + for key, data in pairs(sort_funcs) do + dsorttype:AddChoice(GetTranslation(data.name), key, StringLower(equipment_sorting:GetString()) == key) + end + + dsorttype.OnSelect = function(s, idx, val, data) equipment_sorting:SetString(data) end + + local dsortasc = vgui.Create("DButton", dsort) + dsortasc:SetSize(dsortdirsize, dsortdirsize) + dsortasc:MoveRightOf(dsorttype) + dsortasc:SetY(dsearchpadding) + dsortasc:SetText("") + dsortasc:SetTooltip(GetTranslation("equip_sort_direction_tip")) + + dsortasc.Paint = function(s, pw, ph) + local name = equipment_ascending:GetBool() and "ButtonUp" or "ButtonDown" + derma.SkinHook("Paint", name, s, dsortdirsize, dsortdirsize) + end + + dsortasc.DoClick = function(s) equipment_ascending:SetBool(not equipment_ascending:GetBool()) end + --- Construct icon listing --- icon size = 64 x 64 local dlist = vgui.Create("EquipSelect", dequip) - -- local dlistw = 288 dlist:SetPos(0, dsearchheight + dsearchpadding) dlist:SetSize(dlistw, dlisth - dsearchheight - dsearchpadding) dlist:EnableVerticalScrollbar() dlist:EnableHorizontal(true) + ListPanel = dlist + local bw, bh = 102, 25 -- Whole right column local dih = h - bh - m * 5 - -- local diw = w - dlistw - m*6 - 2 local dinfobg = vgui.Create("DPanel", dequip) dinfobg:SetPaintBackground(false) dinfobg:SetSize(diw - m, dih) @@ -791,21 +971,7 @@ local function TraitorMenuPopup() dhelp:SetSize(diw, 64) dhelp:MoveBelow(dinfo, m) - local update_preqs = PreqLabels(dhelp, m * 7, m * 2) - - local function CannotBuyItem(item) - local orderable = update_preqs(item) - return (not orderable) or - -- already owned - TableHasValue(owned_ids, item.id) or - (tonumber(item.id) and ply:HasEquipmentItem(tonumber(item.id))) or - -- already carrying a weapon for this slot - (ItemIsWeapon(item) and (not CanCarryWeapon(item))) or - -- already bought the item before - (item.limited and ply:HasBought(tostring(item.id))) or - -- doesn't have the required items - not WEPS.PlayerOwnsWepReqs(ply, item) - end + ListPanel.update_preqs = PreqLabels(dhelp, m * 7, m * 2) local function FillEquipmentList(itemlist) dlist:Clear() @@ -813,10 +979,6 @@ local function TraitorMenuPopup() -- temp table for sorting local paneltablefav = {} local paneltable = {} - for i = 0, 9 do - paneltablefav[i] = {} - paneltable[i] = {} - end for _, item in pairs(itemlist) do local ic = nil @@ -877,7 +1039,7 @@ local function TraitorMenuPopup() -- Credit to @Angela and @Technofrood on the Lonely Yogs Discord for the fix! -- Clamp the item slot within the correct limits if ic.slot ~= nil then - ic.slot = math.Clamp(ic.slot, 1, #paneltable) + ic.slot = math.Clamp(ic.slot, 1, 9) end slot:SetIconProperties(COLOR_WHITE, @@ -907,7 +1069,7 @@ local function TraitorMenuPopup() ic:SetTooltip(tip) -- If we cannot order this item, darken it - if CannotBuyItem(item) then + if not CanOrder(item, GetOwnedEquipment()) then ic:SetIconColor(color_darkened) end @@ -917,80 +1079,15 @@ local function TraitorMenuPopup() ic:Remove() else if ic.favorite then - TableInsert(paneltablefav[ic.slot or 1], ic) + TableInsert(paneltablefav, ic) else - TableInsert(paneltable[ic.slot or 1], ic) + TableInsert(paneltable, ic) end end end - local AddNameSortedItems = function(panels) - if sortAlphabetically:GetBool() then - TableSort(panels, function(a, b) return StringLower(a.item.name) < StringLower(b.item.name) end) - end - for _, panel in pairs(panels) do - dlist:AddPanel(panel) - end - end - - -- add favorites first - -- Add equipment items separately - AddNameSortedItems(paneltablefav[0]) - - if sortBySlotFirst:GetBool() then - for i = 1, 9 do - AddNameSortedItems(paneltablefav[i]) - end - else - -- Gather all the panels into one list - local panels = {} - for i = 1, 9 do - for _, p in pairs(paneltablefav[i]) do - TableInsert(panels, p) - end - end - - AddNameSortedItems(panels) - end - - -- non favorites second - -- Randomize positions if this is enabled - if GetConVar("ttt_shop_random_position"):GetBool() then - -- Gather all the panels into one list - local panels = {} - for i = 0, 9 do - for _, p in pairs(paneltable[i]) do - TableInsert(panels, p) - end - end - - -- Randomize it - panels = table.Shuffle(panels) - - -- Add them all to the list - for _, p in ipairs(panels) do - dlist:AddPanel(p) - end - else - -- Add equipment items separately - AddNameSortedItems(paneltable[0]) - - if sortBySlotFirst:GetBool() then - for i = 1, 9 do - AddNameSortedItems(paneltable[i]) - end - else - -- Gather all the panels into one list - local panels = {} - for i = 1, 9 do - for _, p in pairs(paneltable[i]) do - TableInsert(panels, p) - end - end - - AddNameSortedItems(panels) - end - end + AddSortedPanels(paneltablefav) + AddSortedPanels(paneltable) -- select first dlist:SelectPanel(dlist:GetItems()[1]) @@ -1037,7 +1134,7 @@ local function TraitorMenuPopup() -- force a good size. dfields.desc:SetTall(70) - can_order = update_preqs(new.item) + can_order = ListPanel.update_preqs(new.item) else for _, v in pairs(dfields) do if v then @@ -1069,7 +1166,7 @@ local function TraitorMenuPopup() if not pnl or not pnl.item then return end if new:GetPanel() == dequip then - can_order = update_preqs(pnl.item) + can_order = ListPanel.update_preqs(pnl.item) dconfirm:SetEnabled(can_order) end end @@ -1121,7 +1218,7 @@ local function TraitorMenuPopup() local item_panels = dlist:GetItems() local buyable_items = {} for _, item_panel in pairs(item_panels) do - if item_panel.item and not CannotBuyItem(item_panel.item) then + if item_panel.item and CanOrder(item_panel.item, GetOwnedEquipment()) then TableInsert(buyable_items, item_panel) end end diff --git a/gamemodes/terrortown/gamemode/cl_help.lua b/gamemodes/terrortown/gamemode/cl_help.lua index 8dfbcc7a4..9122e7d3b 100644 --- a/gamemodes/terrortown/gamemode/cl_help.lua +++ b/gamemodes/terrortown/gamemode/cl_help.lua @@ -982,17 +982,20 @@ function HELPSCRN:CreateConfig(dsettings) dlanguage:SetName(GetTranslation("set_title_lang")) local dlang = vgui.Create("DComboBox", dlanguage) - dlang:SetConVar("ttt_language") - dlang:AddChoice("Server default", "auto") + dlang:AddChoice("Server default", "server default") + dlang:AddChoice("GMod language setting", "auto") for _, lang in pairs(LANG.GetLanguages()) do dlang:AddChoice(string.Capitalize(lang), lang) end + + local ttt_language = LANG.GetLanguageName(GetConVar("ttt_language"):GetString()) + dlang:SetValue(dlang:GetOptionTextByData(ttt_language)) + -- Why is DComboBox not updating the cvar by default? - dlang.OnSelect = function(idx, val, data) + dlang.OnSelect = function(s, idx, val, data) RunConsoleCommand("ttt_language", data) end - dlang.Think = dlang.ConVarStringThink dlanguage:Help(GetTranslation("set_lang")) dlanguage:AddItem(dlang) diff --git a/gamemodes/terrortown/gamemode/cl_hud.lua b/gamemodes/terrortown/gamemode/cl_hud.lua index 694dba182..6f61854bb 100644 --- a/gamemodes/terrortown/gamemode/cl_hud.lua +++ b/gamemodes/terrortown/gamemode/cl_hud.lua @@ -567,7 +567,12 @@ local function InfoPaint(client) if hide_role:GetBool() then text = GetTranslation("hidden") else - text = LANG.GetRawTranslation(client:GetRoleStringRaw()) or client:GetRoleString() + local role_string = client:GetRoleString() + if role_string ~= ROLE_STRINGS_DEFAULT[client:GetRole()] then + text = role_string + else + text = LANG.GetRawTranslation(client:GetRoleStringRaw()) or role_string + end local new_text = CallHook("TTTHUDRoleNameOverride", nil, client, text) if new_text then text = new_text end diff --git a/gamemodes/terrortown/gamemode/cl_init.lua b/gamemodes/terrortown/gamemode/cl_init.lua index cfafaa0de..30db81c87 100644 --- a/gamemodes/terrortown/gamemode/cl_init.lua +++ b/gamemodes/terrortown/gamemode/cl_init.lua @@ -48,6 +48,7 @@ include("corpse_shd.lua") include("player_ext_shd.lua") include("weaponry_shd.lua") include("sprint_shd.lua") +include("rolepacks_shd.lua") include("vgui/ColoredBox.lua") include("vgui/SimpleIcon.lua") diff --git a/gamemodes/terrortown/gamemode/cl_lang.lua b/gamemodes/terrortown/gamemode/cl_lang.lua index ec83c913b..f8ff1254f 100644 --- a/gamemodes/terrortown/gamemode/cl_lang.lua +++ b/gamemodes/terrortown/gamemode/cl_lang.lua @@ -18,6 +18,7 @@ local string = string local table = table LANG.Strings = {} +LANG.Codes = {} local ttt_language = CreateConVar("ttt_language", "auto", FCVAR_ARCHIVE) @@ -28,10 +29,15 @@ LANG.ServerLanguage = "english" local cached_default, cached_active -function LANG.CreateLanguage(raw_lang_name) +function LANG.CreateLanguage(raw_lang_name, lang_code) if not raw_lang_name then return end local lang_name = string.lower(raw_lang_name) + if lang_code then + lang_code = string.lower(lang_code) + LANG.Codes[lang_code] = lang_name + end + if not LANG.IsLanguage(lang_name) then LANG.Strings[lang_name] = { -- Empty string is very convenient to have, so init with that. @@ -55,7 +61,18 @@ function LANG.CreateLanguage(raw_lang_name) end return LANG.Strings[lang_name] - end +end + +function LANG.GetLanguageName(lang_name) + if not lang_name then return end + lang_name = string.lower(lang_name) + + if LANG.Codes[lang_name] then + lang_name = LANG.Codes[lang_name] + end + + return lang_name +end -- Add a string to a language. Should not be used in a language file, only for -- adding strings elsewhere, such as a SWEP script. @@ -138,7 +155,7 @@ function LANG.GetLanguageTable(lang_name) end function LANG.SetActiveLanguage(lang_name) - lang_name = lang_name and string.lower(lang_name) + lang_name = LANG.GetLanguageName(lang_name) if LANG.IsLanguage(lang_name) then local old_name = LANG.ActiveLanguage @@ -164,6 +181,7 @@ function LANG.SetActiveLanguage(lang_name) end end +local gmod_language = GetConVar("gmod_language") function LANG.Init() local lang_name = ttt_language:GetString() @@ -171,6 +189,8 @@ function LANG.Init() -- we hear from the server which one it is, for now use default if LANG.IsServerDefault(lang_name) then lang_name = LANG.ServerLanguage + elseif LANG.IsAuto(lang_name) then + lang_name = gmod_language:GetString() end LANG.SetActiveLanguage(lang_name) @@ -178,7 +198,12 @@ end function LANG.IsServerDefault(lang_name) lang_name = string.lower(lang_name) - return lang_name == "server default" or lang_name == "auto" + return lang_name == "server default" +end + +function LANG.IsAuto(lang_name) + lang_name = string.lower(lang_name) + return lang_name == "auto" end function LANG.IsLanguage(lang_name) @@ -187,14 +212,15 @@ function LANG.IsLanguage(lang_name) end local function LanguageChanged(cv, old, new) - if new and new ~= LANG.ActiveLanguage then + if not new or new == LANG.ActiveLanguage then return end - if LANG.IsServerDefault(new) then - new = LANG.ServerLanguage - end - - LANG.SetActiveLanguage(new) + if LANG.IsServerDefault(new) then + new = LANG.ServerLanguage + elseif LANG.IsAuto(new) then + new = gmod_language:GetString() end + + LANG.SetActiveLanguage(new) end cvars.AddChangeCallback("ttt_language", LanguageChanged) @@ -203,6 +229,13 @@ local function ForceReload() end concommand.Add("ttt_reloadlang", ForceReload) +local function GMLanguageChanged(cv, old, new) + if new and LANG.IsAuto(ttt_language:GetString()) then + LANG.SetActiveLanguage(new) + end +end +cvars.AddChangeCallback("gmod_language", GMLanguageChanged) + -- Get a copy of all available languages (keys in the Strings tbl) function LANG.GetLanguages() local langs = {} diff --git a/gamemodes/terrortown/gamemode/cl_scoring.lua b/gamemodes/terrortown/gamemode/cl_scoring.lua index 69c4cd0cf..7ae50f2e6 100644 --- a/gamemodes/terrortown/gamemode/cl_scoring.lua +++ b/gamemodes/terrortown/gamemode/cl_scoring.lua @@ -412,9 +412,10 @@ function CLSCORE:BuildScorePanel(dpanel) local skull = vgui.Create("DImage", surv) skull:SetMaterial(skull_icon) - skull:SetTooltip("Dead") + skull:SetTooltip(T("dead")) skull:SetKeepAspect(true) skull:SetSize(18, 18) + skull:SetMouseInputEnabled(true) end local points_own = KillsToPoints(s, was_traitor, was_innocent) diff --git a/gamemodes/terrortown/gamemode/cl_scoring_events.lua b/gamemodes/terrortown/gamemode/cl_scoring_events.lua index d2aaebc0a..8028f2a44 100644 --- a/gamemodes/terrortown/gamemode/cl_scoring_events.lua +++ b/gamemodes/terrortown/gamemode/cl_scoring_events.lua @@ -116,7 +116,7 @@ Event(EVENT_GAME, if e.state == ROUND_ACTIVE then return T("ev_start") end end, icon = function(e) - return app_icon, "Game" + return app_icon, T("ev_start_tip") end }) @@ -132,7 +132,7 @@ Event(EVENT_SPAWN, end end, icon = function(e) - return app_icon, "Game" + return app_icon, T("ev_start_tip") end }) Event(EVENT_ROLECHANGE, @@ -146,7 +146,7 @@ Event(EVENT_ROLECHANGE, end end, icon = function(e) - return app_icon, "Game" + return app_icon, T("ev_start_tip") end }) @@ -158,7 +158,7 @@ Event(EVENT_CREDITFOUND, player = e.b}) end, icon = function(e) - return credit_icon, "Credit found" + return credit_icon, T("ev_credit_tip") end }) @@ -167,7 +167,7 @@ Event(EVENT_BODYFOUND, return PT("ev_body", {finder = e.ni, victim = e.b}) end, icon = function(e) - return magnifier_icon, "Body discovered" + return magnifier_icon, T("ev_body_tip") end }) @@ -178,7 +178,7 @@ Event(EVENT_C4DISARM, {player = e.ni, owner = e.own or "aliens"}) end, icon = function(e) - return wrench_icon, "C4 disarm" + return wrench_icon, T("ev_c4_disarm_tip") end }) @@ -187,7 +187,7 @@ Event(EVENT_C4EXPLODE, return PT("ev_c4_boom", {player = e.ni}) end, icon = function(e) - return bomb_icon, "C4 exploded" + return bomb_icon, T("ev_c4_boom_tip") end }) @@ -196,7 +196,7 @@ Event(EVENT_C4PLANT, return PT("ev_c4_plant", {player = e.ni}) end, icon = function(e) - return bomb_icon, "C4 planted" + return bomb_icon, T("ev_c4_plant_tip") end }) @@ -315,17 +315,17 @@ Event(EVENT_KILL, text = KillText, icon = function(e) if e.att.sid64 == e.vic.sid64 or e.att.sid64 == -1 then - return wrong_icon, "Suicide" + return wrong_icon, T("ev_suicide") end - local attacker = (e.att.tr and "Traitor") or (e.att.jes and "Jester") or (e.att.ind and "Independent") or (e.att.mon and "Monster") or "Innocent" - local victim = (e.vic.tr and "Traitor") or (e.vic.jes and "Jester") or (e.vic.ind and "Independent") or (e.vic.mon and "Monster") or "Innocent" + local attacker = (e.att.tr and "traitor") or (e.att.jes and "jester") or (e.att.ind and "independent") or (e.att.mon and "monster") or "innocent" + local victim = (e.vic.tr and "traitor") or (e.vic.jes and "jester") or (e.vic.ind and "independent") or (e.vic.mon and "monster") or "innocent" if e.tk then - return wrong_icon, "Teamkill" + return wrong_icon, T("ev_teamkill") elseif e.att.tr or e.att.ind or e.att.mon then - return right_icon, attacker.." killed "..victim + return right_icon, T(attacker)..T("ev_killed")..T(victim) else - return shield_icon, attacker.." killed "..victim + return shield_icon, T(attacker)..T("ev_killed")..T(victim) end end }) @@ -335,7 +335,7 @@ Event(EVENT_DEFIBRILLATED, { return PT("ev_defi", {victim = e.vic}) end, icon = function(e) - return heart_icon, "Defibrillated" + return heart_icon, T("ev_defi_icon") end}) Event(EVENT_DISCONNECTED, { @@ -343,7 +343,7 @@ Event(EVENT_DISCONNECTED, { return PT("ev_disco", {victim = e.vic}) end, icon = function(e) - return disconnect_icon, "Disconnected" + return disconnect_icon, T("ev_disco_icon") end}) Event(EVENT_LOG, { @@ -351,5 +351,5 @@ Event(EVENT_LOG, { return e.txt end, icon = function(e) - return info_icon, "Information" + return info_icon, T("ev_info_icon") end}) \ No newline at end of file diff --git a/gamemodes/terrortown/gamemode/cl_search.lua b/gamemodes/terrortown/gamemode/cl_search.lua index 03cbf3df3..8f3568e39 100644 --- a/gamemodes/terrortown/gamemode/cl_search.lua +++ b/gamemodes/terrortown/gamemode/cl_search.lua @@ -261,7 +261,7 @@ function PreprocSearch(raw) local vic = Entity(d[1]) local dc = d[1] == 0 -- disconnected if dc or IsPlayer(vic) then - search[t].text = PT("search_kills1", { player = dc and "" or vic:Nick() }) + search[t].text = PT("search_kills1", { player = dc and Format("<%s>", T("disconnected")) or vic:Nick() }) end elseif num > 1 then local txt = T("search_kills2") .. "\n" @@ -271,7 +271,7 @@ function PreprocSearch(raw) local vic = Entity(idx) local dc = idx == 0 if dc or IsPlayer(vic) then - table.insert(nicks, dc and "" or vic:Nick()) + table.insert(nicks, dc and Format("<%s>", T("disconnected")) or vic:Nick()) end end diff --git a/gamemodes/terrortown/gamemode/cl_voice.lua b/gamemodes/terrortown/gamemode/cl_voice.lua index a9353a99a..dfb25dd86 100644 --- a/gamemodes/terrortown/gamemode/cl_voice.lua +++ b/gamemodes/terrortown/gamemode/cl_voice.lua @@ -23,6 +23,12 @@ local RunHook = hook.Run local GetTranslation = LANG.GetTranslation local GetPTranslation = LANG.GetParamTranslation +local LastWordContext = { + [KILL_SUICIDE] = "words_suicide", + [KILL_FALL] = "words_fall", + [KILL_BURN] = "words_burn" +} + local function GetChatPlayerName(ply, team_chat) local name = CallHook("TTTChatPlayerName", nil, ply, team_chat or false) if not name or #name == 0 then @@ -37,16 +43,27 @@ end local function LastWordsRecv() local sender = net.ReadPlayer() local words = net.ReadString() + local death_type = net.ReadUInt(2) + + -- only append "--" if there's no ending interpunction + local final = string.match(words, "[\\.\\!\\?]$") ~= nil + local lastWordsStr = words .. (final and " " or "-- ") + + -- add optional context relating to death type + if death_type ~= KILL_NORMAL then + local context = LastWordContext[death_type] or "" + lastWordsStr = lastWordsStr .. Format("*%s*", GetTranslation(context)) + end local was_detective = IsValid(sender) and sender:IsDetectiveTeam() - local nick = IsValid(sender) and GetChatPlayerName(sender) or "" + local nick = IsValid(sender) and GetChatPlayerName(sender) or Format("<%s>", GetTranslation("unknown")) chat.AddText(Color(150, 150, 150), Format("(%s) ", string.upper(GetTranslation("last_words"))), was_detective and Color(50, 200, 255) or Color(0, 200, 0), nick, COLOR_WHITE, - ": " .. words) + ": " .. lastWordsStr) end net.Receive("TTT_LastWordsMsg", LastWordsRecv) diff --git a/gamemodes/terrortown/gamemode/gamemsg.lua b/gamemodes/terrortown/gamemode/gamemsg.lua index 555ce89ff..80e4d6fec 100644 --- a/gamemodes/terrortown/gamemode/gamemsg.lua +++ b/gamemodes/terrortown/gamemode/gamemsg.lua @@ -431,13 +431,15 @@ local function LastWordsMsg(ply, words) local final = string.match(words, "[\\.\\!\\?]$") ~= nil -- add optional context relating to death type - local context = LastWordContext[ply.death_type] or "" + local death_type = ply.death_type + local context = LastWordContext[death_type] or "" local lastWordsStr = words .. (final and "" or "--") .. context - if RunHook("TTTLastWordsMsg", ply, lastWordsStr, words) ~= true then + if RunHook("TTTLastWordsMsg", ply, lastWordsStr, words, death_type) ~= true then net.Start("TTT_LastWordsMsg") net.WritePlayer(ply) - net.WriteString(lastWordsStr) + net.WriteString(words) + net.WriteUInt(death_type, 2) net.Broadcast() end end diff --git a/gamemodes/terrortown/gamemode/init.lua b/gamemodes/terrortown/gamemode/init.lua index 7da646d3c..4c2265892 100644 --- a/gamemodes/terrortown/gamemode/init.lua +++ b/gamemodes/terrortown/gamemode/init.lua @@ -50,6 +50,7 @@ AddCSLuaFile("cl_sprint.lua") AddCSLuaFile("sprint_shd.lua") AddCSLuaFile("cl_cheatsheet.lua") AddCSLuaFile("cl_sync.lua") +AddCSLuaFile("rolepacks_shd.lua") include("shared.lua") include("utf8_ext.lua") @@ -74,6 +75,7 @@ include("hitmarkers.lua") include("deathnotify.lua") include("roleweapons.lua") include("sprint_shd.lua") +include("rolepacks_shd.lua") include("rolepacks.lua") include("roleblocks.lua") include("sync.lua") @@ -625,6 +627,10 @@ function PrepareRound() -- Update damage scaling KARMA.RoundBegin() + ROLEPACKS.SendRolePackRoleList() + ROLEPACKS.ApplyRolePackConVars() + ROLEPACKS.FillRolePackWeaponTables() + WEPS.ResetWeaponsCache() WEPS.ResetRoleWeaponCache() WEPS.ClearRetryTimers() diff --git a/gamemodes/terrortown/gamemode/lang/english.lua b/gamemodes/terrortown/gamemode/lang/english.lua index 1366fc46d..4ef534ffd 100644 --- a/gamemodes/terrortown/gamemode/lang/english.lua +++ b/gamemodes/terrortown/gamemode/lang/english.lua @@ -1,6 +1,6 @@ ---- English language strings -local L = LANG.CreateLanguage("English") +local L = LANG.CreateLanguage("English", "en") --- General text used in various places L.hidden = "Hidden" @@ -150,10 +150,6 @@ L.radio_button_huge = "H.U.G.E burst" L.radio_button_c4 = "C4 beeping" L.radio_button_burn = "Burning" L.radio_button_steps = "Footsteps" -L.radio_button_glock = "Glock shots" -L.radio_button_sipist = "Silenced shots" -L.radio_button_tele = "Teleport" -L.radio_button_heal = "Healing" -- Intro screen shown after joining L.intro_help = "If you're new to the game, press F1 for instructions!" @@ -287,8 +283,6 @@ L.set_wswitch_tip = "By default the weapon switcher automatically closes a few s L.set_swselect = "Close menu when weapon selected" L.set_swselect_tip = "By default the weapon switcher closes when a weapon is selected. Disable this to make it stay up. Ignored when fast switching is enabled." L.set_cues = "Play sound cue when a round begins or ends" -L.set_msg_cue = "Play sound cue when a notification appears" -L.set_msg_cue_tip = "Whether to play a sound whenever a popup message appears (Popup messages appear in the top right corner when rounds begin/end and when bodies are found/searched)" L.set_raw_karma = "Show the raw karma value" L.set_raw_karma_tip = "Shows the raw karma value in the scoreboard instead of the percentage of damage each player deals" L.set_karma_total_pct = "Show karma as percent of total" @@ -1195,6 +1189,10 @@ L.idle_popup_title = "Idle" --- 2021-06-07 L.sb_playervolume = "Player Volume" +--- 2023-07-28 +L.set_msg_cue = "Play sound cue when a notification appears" +L.set_msg_cue_tip = "Whether to play a sound whenever a popup message appears (Popup messages appear in the top right corner when rounds begin/end and when bodies are found/searched)" + --- 2025-03-11 L.set_title_cross = "Crosshair settings" @@ -1204,9 +1202,47 @@ L.set_hip_cross_opacity = "Opacity" L.set_cross_thickness = "Thickness" L.set_cross_outlinethickness = "Outline thickness" +--- 2025-09-04 +L.radio_button_glock = "Glock shots" +L.radio_button_sipist = "Silenced shots" +L.radio_button_tele = "Teleport" +L.radio_button_heal = "Healing" + +--- 2026-02-27 +L.dead = "Dead" +L.disconnected = "Disconnected" +L.unknown = "Unknown" + +L.words_suicide = "kills self" +L.words_fall = "SPLUT" +L.words_burn = "crackle" + +L.ev_start_tip = "Game" +L.ev_credit_tip = "Credit found" +L.ev_body_tip = "Body discovered" + +L.ev_c4_disarm_tip = "C4 disarm" +L.ev_c4_boom_tip = "C4 exploded" +L.ev_c4_plant_tip = "C4 planted" + +L.ev_suicide = "Suicide" +L.ev_teamkill = "Teamkill" +L.ev_killed = "killed" + +-- 2026-03-11 +L.equip_sort_default = "Default" +L.equip_sort_slot = "Slot" + +L.equip_sort_direction_tip = "Sort direction" + +L.set_hide_unbuyable = "Move unbuyable equipment items to the bottom of the list" + -- Custom Events L.ev_defi = "{victim} was respawned" +L.ev_defi_icon = "Defibrillated" L.ev_disco = "{victim} disconnected" +L.ev_disco_icon = "Disconnected" +L.ev_info_icon = "Information" -- Role Weapons Configuration L.roleweapons_title = "Role Weapons Configuration" diff --git a/gamemodes/terrortown/gamemode/player.lua b/gamemodes/terrortown/gamemode/player.lua index 7ce31de84..0a00416e1 100644 --- a/gamemodes/terrortown/gamemode/player.lua +++ b/gamemodes/terrortown/gamemode/player.lua @@ -60,7 +60,7 @@ function GM:PlayerInitialSpawn(ply) ply:SetForceSpec(true) end - ROLEPACKS.SendRolePackRoleList(ply) + ROLEPACKS.SendRolePackRoleList(ply, rstate == ROUND_ACTIVE) ROLEPACKS.SendRolePackWeapons(ply) WEPS.HandleRoleEquipment(ply) end @@ -978,7 +978,7 @@ function GM:ScalePlayerDamage(ply, hitgroup, dmginfo) local wep = util.WeaponFromDamage(dmginfo) - if IsValid(wep) and not GetConVar("ttt_disable_headshots"):GetBool() then + if IsValid(wep) and wep.GetHeadshotMultiplier and not GetConVar("ttt_disable_headshots"):GetBool() then local s = wep:GetHeadshotMultiplier(ply, dmginfo) or 2 dmginfo:ScaleDamage(s) end diff --git a/gamemodes/terrortown/gamemode/roleblocks.lua b/gamemodes/terrortown/gamemode/roleblocks.lua index 09015d565..e9e3ef3d8 100644 --- a/gamemodes/terrortown/gamemode/roleblocks.lua +++ b/gamemodes/terrortown/gamemode/roleblocks.lua @@ -112,8 +112,8 @@ function ROLEBLOCKS.GetBlockedRoles(excludeRolePack) local blocks = {} excludeRolePack = excludeRolePack or false - local rolepack = GetConVar("ttt_role_pack"):GetString() - if rolepack and #rolepack > 0 and not excludeRolePack then + local rolepack = ROLEPACKS.GetCurrentRolePackName() + if #rolepack > 0 and not excludeRolePack then local rolepackblocks = ROLEPACKS.GetRolePackBlockedRoles() if rolepackblocks and rolepackblocks.groups then blocks = table.Copy(rolepackblocks.groups) diff --git a/gamemodes/terrortown/gamemode/rolepacks.lua b/gamemodes/terrortown/gamemode/rolepacks.lua index 949c8db32..8f210bd97 100644 --- a/gamemodes/terrortown/gamemode/rolepacks.lua +++ b/gamemodes/terrortown/gamemode/rolepacks.lua @@ -14,8 +14,6 @@ local TableHasValue = table.HasValue local TableShuffle = table.Shuffle local TableCopy = table.Copy -ROLEPACKS = {} - util.AddNetworkString("TTT_WriteRolePackRoles") util.AddNetworkString("TTT_WriteRolePackRoles_Part") util.AddNetworkString("TTT_RequestRolePackRoles") @@ -312,7 +310,7 @@ net.Receive("TTT_SavedRolePack", function(len, ply) end local savedPack = net.ReadString() - local currentPack = GetConVar("ttt_role_pack"):GetString() + local currentPack = ROLEPACKS.GetCurrentRolePackName() if savedPack == currentPack then ROLEPACKS.SendRolePackRoleList() ROLEPACKS.ApplyRolePackConVars() @@ -378,12 +376,13 @@ function ROLEPACKS.GetRolePackDetails(name) return jsonTable.details or {} end -function ROLEPACKS.SendRolePackRoleList(ply) +function ROLEPACKS.SendRolePackRoleList(ply, lateJoin) ROLE_PACK_ROLES = {} ROLE_PACK_DETAILS = {} net.Start("TTT_SendRolePackRoleList") - local name = GetConVar("ttt_role_pack"):GetString() + -- If we're sending to a late joiner, use the current running role pack + local name = lateJoin and ROLEPACKS.GetCurrentRolePackName() or GetConVar("ttt_role_pack"):GetString() local json = file.Read("rolepacks/" .. name .. "/roles.json", "DATA") if not json then net.WriteUInt(0, 8) @@ -435,6 +434,7 @@ function ROLEPACKS.SendRolePackRoleList(ply) end ROLE_PACK_DETAILS = jsonTable.details or {} + ROLE_PACK_DETAILS.name = name net.WriteTable(ROLE_PACK_DETAILS, false) if ply then @@ -445,7 +445,7 @@ function ROLEPACKS.SendRolePackRoleList(ply) end function ROLEPACKS.GetRolePackBlockedRoles() - local name = GetConVar("ttt_role_pack"):GetString() + local name = ROLEPACKS.GetCurrentRolePackName() local json = file.Read("rolepacks/" .. name .. "/roleblocks.json", "DATA") if not json then return end @@ -459,7 +459,7 @@ function ROLEPACKS.GetRolePackBlockedRoles() end function ROLEPACKS.SendRolePackWeapons(ply) - local rolePackName = GetConVar("ttt_role_pack"):GetString() + local rolePackName = ROLEPACKS.GetCurrentRolePackName() if #rolePackName == 0 then return end for id, _ in pairs(ROLE_STRINGS_RAW) do @@ -489,7 +489,7 @@ function ROLEPACKS.SendRolePackWeapons(ply) end function ROLEPACKS.AssignRoles(choices) - local rolePackName = GetConVar("ttt_role_pack"):GetString() + local rolePackName = ROLEPACKS.GetCurrentRolePackName() if #rolePackName == 0 then return end local json = file.Read("rolepacks/" .. rolePackName .. "/roles.json", "DATA") @@ -669,7 +669,7 @@ function ROLEPACKS.ApplyRolePackConVars() ROLEPACKS.OldConVars[cvar] = nil end - local rolePackName = GetConVar("ttt_role_pack"):GetString() + local rolePackName = ROLEPACKS.GetCurrentRolePackName() if #rolePackName == 0 then return end local json = file.Read("rolepacks/" .. rolePackName .. "/convars.json", "DATA") @@ -717,7 +717,7 @@ function ROLEPACKS.ApplyRolePackConVars() end function ROLEPACKS.FillRolePackWeaponTables() - local rolePackName = GetConVar("ttt_role_pack"):GetString() + local rolePackName = ROLEPACKS.GetCurrentRolePackName() if #rolePackName == 0 then return end local handled = false @@ -766,6 +766,7 @@ function ROLEPACKS.FillRolePackWeaponTables() end cvars.AddChangeCallback("ttt_role_pack", function(cvar, old, new) + if GetRoundState() == ROUND_ACTIVE then return end ROLEPACKS.SendRolePackRoleList() ROLEPACKS.ApplyRolePackConVars() ROLEPACKS.FillRolePackWeaponTables() diff --git a/gamemodes/terrortown/gamemode/rolepacks_shd.lua b/gamemodes/terrortown/gamemode/rolepacks_shd.lua new file mode 100644 index 000000000..1cc6f3b91 --- /dev/null +++ b/gamemodes/terrortown/gamemode/rolepacks_shd.lua @@ -0,0 +1,10 @@ +local table = table + +ROLEPACKS = ROLEPACKS or {} + +function ROLEPACKS.GetCurrentRolePackName() + if ROLE_PACK_DETAILS and not table.IsEmpty(ROLE_PACK_DETAILS) then + return ROLE_PACK_DETAILS.name or "" + end + return "" +end \ No newline at end of file diff --git a/gamemodes/terrortown/gamemode/roles/cannibal/cannibal.lua b/gamemodes/terrortown/gamemode/roles/cannibal/cannibal.lua index e029f6505..a3fe91b12 100644 --- a/gamemodes/terrortown/gamemode/roles/cannibal/cannibal.lua +++ b/gamemodes/terrortown/gamemode/roles/cannibal/cannibal.lua @@ -39,6 +39,9 @@ local function ReleaseEatenPlayers(ply, message) v:DrawViewModel(true) v:DrawWorldModel(true) v:SetNoDraw(false) + if IsValid(v.hat) then + v.hat:SetNoDraw(false) + end v:Spawn() local pos = ply:GetPos() v:SetPos(FindRespawnLocation(pos) or pos) @@ -46,6 +49,8 @@ local function ReleaseEatenPlayers(ply, message) local sID64 = v:SteamID64() + timer.Remove("TTTCannibalDigestion_" .. sID64) + for _, data in ipairs(CANNIBAL.playerWeapons[sID64]) do local wep = v:Give(data.class) if not IsValid(wep) then continue end diff --git a/gamemodes/terrortown/gamemode/roles/cannibal/cl_cannibal.lua b/gamemodes/terrortown/gamemode/roles/cannibal/cl_cannibal.lua index a57af1f9d..583adb885 100644 --- a/gamemodes/terrortown/gamemode/roles/cannibal/cl_cannibal.lua +++ b/gamemodes/terrortown/gamemode/roles/cannibal/cl_cannibal.lua @@ -189,9 +189,33 @@ AddHook("TTTTutorialRoleText", "Cannibal_TTTTutorialRoleText", function(role, ti local roleTeamName, roleColor = GetRoleTeamInfo(roleTeam) local html = "The " .. ROLE_STRINGS[ROLE_CANNIBAL] .. " is a " .. roleTeamName .. " role whose goal is to eat all other players." - html = html .. "Eaten players are not dead, but they are unable to do anything except talk with other eaten players and spectate the " .. ROLE_STRINGS[ROLE_CANNIBAL] .. "." + if GetConVar("ttt_cannibal_gains_health"):GetBool() then + local gained_health_percentage = GetConVar("ttt_cannibal_gained_health_percentage"):GetInt() + local gained + if gained_health_percentage > 0 then + gained = gained_health_percentage .. "% of the victim's health" + else + gained = "100HP" + end - html = html .. "If the " .. ROLE_STRINGS[ROLE_CANNIBAL] .. " dies, all eaten players are freed at the position where the " .. ROLE_STRINGS[ROLE_CANNIBAL] .. " died." + html = html .. "When the " .. ROLE_STRINGS[ROLE_CANNIBAL] .. " eats a player, they gain " .. gained .. " immediately." + end + + local eatenDetail = "" + local deathDetail = "" + + if GetConVar("ttt_cannibal_digestion"):GetBool() then + eatenDetail = " immediately" + deathDetail = " undigested" + end + + html = html .. "Eaten players are not" .. eatenDetail .. " dead, but they are unable to do anything except talk with other eaten players and spectate the " .. ROLE_STRINGS[ROLE_CANNIBAL] .. "." + + if GetConVar("ttt_cannibal_digestion"):GetBool() then + html = html .. "Eaten players are digested and killed " .. GetConVar("ttt_cannibal_digestion_time"):GetInt() .. " seconds after being eaten." + end + + html = html .. "If the " .. ROLE_STRINGS[ROLE_CANNIBAL] .. " dies, all" .. deathDetail .. " eaten players are freed at the position where the " .. ROLE_STRINGS[ROLE_CANNIBAL] .. " died." return html end diff --git a/gamemodes/terrortown/gamemode/roles/cannibal/shared.lua b/gamemodes/terrortown/gamemode/roles/cannibal/shared.lua index 75e2f9c46..140ab19e5 100644 --- a/gamemodes/terrortown/gamemode/roles/cannibal/shared.lua +++ b/gamemodes/terrortown/gamemode/roles/cannibal/shared.lua @@ -49,6 +49,30 @@ table.insert(ROLE_CONVARS[ROLE_CANNIBAL], { cvar = "ttt_cannibal_update_scoreboard", type = ROLE_CONVAR_TYPE_BOOL }) +table.insert(ROLE_CONVARS[ROLE_CANNIBAL], { + cvar = "ttt_cannibal_gains_health", + type = ROLE_CONVAR_TYPE_BOOL +}) +table.insert(ROLE_CONVARS[ROLE_CANNIBAL], { + cvar = "ttt_cannibal_gained_health_percentage", + type = ROLE_CONVAR_TYPE_NUM +}) +table.insert(ROLE_CONVARS[ROLE_CANNIBAL], { + cvar = "ttt_cannibal_digestion", + type = ROLE_CONVAR_TYPE_BOOL +}) +table.insert(ROLE_CONVARS[ROLE_CANNIBAL], { + cvar = "ttt_cannibal_digestion_time", + type = ROLE_CONVAR_TYPE_NUM +}) +table.insert(ROLE_CONVARS[ROLE_CANNIBAL], { + cvar = "ttt_cannibal_digestion_poop", + type = ROLE_CONVAR_TYPE_BOOL +}) +table.insert(ROLE_CONVARS[ROLE_CANNIBAL], { + cvar = "ttt_cannibal_digestion_poop_sound", + type = ROLE_CONVAR_TYPE_BOOL +}) ------------------- -- ROLE FEATURES -- diff --git a/gamemodes/terrortown/gamemode/roles/drunk/drunk.lua b/gamemodes/terrortown/gamemode/roles/drunk/drunk.lua index 967aac977..2bdcb8a98 100644 --- a/gamemodes/terrortown/gamemode/roles/drunk/drunk.lua +++ b/gamemodes/terrortown/gamemode/roles/drunk/drunk.lua @@ -175,7 +175,7 @@ function plymeta:DrunkJoinLosingTeam() local traitorPct = traitors / (players - jestersIndependents - monsters) -- If a role pack is enabled calculate the expected ratio of innocents to traitors - local rolePack = GetConVar("ttt_role_pack"):GetString() + local rolePack = ROLEPACKS.GetCurrentRolePackName() if #rolePack > 0 then local json = file.Read("rolepacks/" .. rolePack .. "/roles.json", "DATA") if json then diff --git a/gamemodes/terrortown/gamemode/roles/twins/twins.lua b/gamemodes/terrortown/gamemode/roles/twins/twins.lua index 192fafd39..521e11956 100644 --- a/gamemodes/terrortown/gamemode/roles/twins/twins.lua +++ b/gamemodes/terrortown/gamemode/roles/twins/twins.lua @@ -112,6 +112,7 @@ hook.Add("TTTBeginRound", "Twins_TTTBeginRound", function() for _, p in ipairs(evilTwins) do p:SetRole(ROLE_TRAITOR) end + SendFullStateUpdate() return end @@ -243,6 +244,70 @@ hook.Add("PlayerDeath", "Twins_PlayerDeath", function(victim, infl, attacker) end) hook.Add("TTTPlayerRoleChanged", "Twins_TTTPlayerRoleChanged", function(ply, oldRole, newRole) + -- If someone changes to a twin after the round starts, handle that specific case + if GetRoundState() == ROUND_ACTIVE and (newRole == ROLE_GOODTWIN or newRole == ROLE_EVILTWIN) then + local goodTwins = {} + local evilTwins = {} + local otherTwins = {} + for _, p in player.Iterator() do + if not p:IsActive() then continue end + + if p ~= ply then + table.insert(otherTwins, p) + end + + if p:IsGoodTwin() then + table.insert(goodTwins, p) + elseif p:IsEvilTwin() then + table.insert(evilTwins, p) + end + end + + -- If we don't have both kinds of twin, change this player + -- to the base role for their team + if #goodTwins == 0 or #evilTwins == 0 then + local role + if newRole == ROLE_GOODTWIN then + role = ROLE_INNOCENT + else + role = ROLE_TRAITOR + end + ply:SetRole(role) + SendFullStateUpdate() + return + end + + -- Remove this player so we don't tell them about themselves + table.RemoveByValue(goodTwins, ply) + table.RemoveByValue(evilTwins, ply) + + -- Otherwise let everyone know about the new twin + local message = "You have a new " .. ROLE_STRINGS[ply:GetRole()] .. ", " .. ply:Nick() .. "!" + for _, p in ipairs(otherTwins) do + p:QueueMessage(MSG_PRINTBOTH, message) + end + + -- And let the new twin know about their twins + if #goodTwins > 0 then + if #goodTwins == 1 then + message = goodTwins[1]:Nick() .. " is your " .. ROLE_STRINGS[ROLE_GOODTWIN] .. "." + else + message = "You have multiple " .. ROLE_STRINGS_PLURAL[ROLE_GOODTWIN] .. "! They are " .. util.FormattedList(goodTwins, function(p) return p:Nick() end) .. "." + end + ply:QueueMessage(MSG_PRINTBOTH, message) + end + + if #evilTwins > 0 then + if #evilTwins == 1 then + message = evilTwins[1]:Nick() .. " is your " .. ROLE_STRINGS[ROLE_EVILTWIN] .. "." + else + message = "You have multiple " .. ROLE_STRINGS_PLURAL[ROLE_EVILTWIN] .. "! They are " .. util.FormattedList(evilTwins, function(p) return p:Nick() end) .. "." + end + ply:QueueMessage(MSG_PRINTBOTH, message) + end + return + end + CheckTwinsInvulnerability(ply, oldRole) if (oldRole == ROLE_GOODTWIN or oldRole == ROLE_EVILTWIN) and newRole ~= ROLE_GOODTWIN and newRole ~= ROLE_EVILTWIN then if timer.Exists("TwinInvulnerabilityEnd_" .. ply:SteamID64()) then diff --git a/gamemodes/terrortown/gamemode/roles/zombie/zombie.lua b/gamemodes/terrortown/gamemode/roles/zombie/zombie.lua index 0dccd1fa4..6e455eb5a 100644 --- a/gamemodes/terrortown/gamemode/roles/zombie/zombie.lua +++ b/gamemodes/terrortown/gamemode/roles/zombie/zombie.lua @@ -386,8 +386,8 @@ hook.Add("SetupPlayerVisibility", "Zombie_SetupPlayerVisibility", function(ply) if not zombie_vision_enabled:GetBool() and not zombie_show_target_icon:GetBool() then return end -- Only use this when the zombie would see the highlighting and icons (when they have their claws out) - local hasFangs = ply.GetActiveWeapon and IsValid(ply:GetActiveWeapon()) and ply:GetActiveWeapon():GetClass() == "weapon_zom_claws" - if not hasFangs then return end + local hasClaws = ply.GetActiveWeapon and IsValid(ply:GetActiveWeapon()) and ply:GetActiveWeapon():GetClass() == "weapon_zom_claws" + if not hasClaws then return end for _, v in PlayerIterator() do if ply:TestPVS(v) then continue end diff --git a/gamemodes/terrortown/gamemode/shared.lua b/gamemodes/terrortown/gamemode/shared.lua index 957ea529d..e610042b3 100644 --- a/gamemodes/terrortown/gamemode/shared.lua +++ b/gamemodes/terrortown/gamemode/shared.lua @@ -27,7 +27,7 @@ local Utf8Sub = utf8.sub include("player_class/player_ttt.lua") -- Version string for display and function for version checks -CR_VERSION = "2.4.3" +CR_VERSION = "2.4.4" CR_BETA = true CR_WORKSHOP_ID = CR_BETA and "2404251054" or "2421039084" @@ -660,6 +660,8 @@ ROLE_STRINGS = { [ROLE_TASKMASTER] = "Taskmaster" } +ROLE_STRINGS_DEFAULT = table.Copy(ROLE_STRINGS) + ROLE_STRINGS_PLURAL = { [ROLE_INNOCENT] = "Innocents", [ROLE_TRAITOR] = "Traitors", @@ -989,6 +991,7 @@ function RegisterRole(tbl) ROLE_STRINGS_RAW[roleID] = tbl.nameraw ROLE_STRINGS[roleID] = tbl.name + ROLE_STRINGS_DEFAULT[roleID] = tbl.name ROLE_STRINGS_PLURAL[roleID] = tbl.nameplural ROLE_STRINGS_EXT[roleID] = tbl.nameext ROLE_STRINGS_SHORT[roleID] = tbl.nameshort diff --git a/gamemodes/terrortown/gamemode/weaponry.lua b/gamemodes/terrortown/gamemode/weaponry.lua index 4ddef1015..bf7e18be5 100644 --- a/gamemodes/terrortown/gamemode/weaponry.lua +++ b/gamemodes/terrortown/gamemode/weaponry.lua @@ -246,7 +246,7 @@ function GM:PlayerLoadout(ply) -- Gather the list of extra loadout weapons for this role local role = ply:GetRole() local mergedLoadoutWeapons = TableCopy(WEPS.LoadoutWeapons[role] or {}) - local packName = GetConVar("ttt_role_pack"):GetString() + local packName = ROLEPACKS.GetCurrentRolePackName() if #packName > 0 then for _, v in pairs(WEPS.RolePackLoadoutWeapons[role] or {}) do if not TableHasValue(mergedLoadoutWeapons, v) then diff --git a/gamemodes/terrortown/gamemode/weaponry_shd.lua b/gamemodes/terrortown/gamemode/weaponry_shd.lua index 3cfc8242c..df3065a0c 100644 --- a/gamemodes/terrortown/gamemode/weaponry_shd.lua +++ b/gamemodes/terrortown/gamemode/weaponry_shd.lua @@ -293,7 +293,7 @@ function WEPS.HandleCanBuyOverrides(wep, role, block_randomization, sync_traitor WEPS.PrepWeaponsLists(role) - local packName = GetConVar("ttt_role_pack"):GetString() + local packName = ROLEPACKS.GetCurrentRolePackName() if rolepack_weps == nil and #packName > 0 then rolepack_weps = {Buyables = WEPS.RolePackBuyableWeapons[role], Excludes = WEPS.RolePackExcludeWeapons[role], NoRandoms = WEPS.RolePackBypassRandomWeapons[role]} elseif rolepack_weps == false or #packName == 0 then diff --git a/templates/role/Role Addon Template/lua/customroles/%NAMERAW%.lua b/templates/role/Role Addon Template/lua/customroles/%NAMERAW%.lua index a967b9676..e5b0e4d57 100644 --- a/templates/role/Role Addon Template/lua/customroles/%NAMERAW%.lua +++ b/templates/role/Role Addon Template/lua/customroles/%NAMERAW%.lua @@ -8,6 +8,8 @@ ROLE.nameshort = "" ROLE.desc = [[]] +ROLE.shortdesc = "" + ROLE.team = ROLE_TEAM_ ROLE.shop = nil