├── .gitignore ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md └── src ├── Custom Assetbundle.0a61c6.ttslua ├── Custom Assetbundle.0a61c6.xml ├── Custom Assetbundle.3f75b3.ttslua ├── Custom Assetbundle.f3cdf2.ttslua ├── Global.-1.ttslua ├── Global.-1.xml └── TS_Save.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Lua sources 2 | luac.out 3 | 4 | # luarocks build files 5 | *.src.rock 6 | *.zip 7 | *.tar.gz 8 | 9 | # Object files 10 | *.o 11 | *.os 12 | *.ko 13 | *.obj 14 | *.elf 15 | 16 | # Precompiled Headers 17 | *.gch 18 | *.pch 19 | 20 | # Libraries 21 | *.lib 22 | *.a 23 | *.la 24 | *.lo 25 | *.def 26 | *.exp 27 | 28 | # Shared objects (inc. Windows DLLs) 29 | *.dll 30 | *.so 31 | *.so.* 32 | *.dylib 33 | 34 | # Executables 35 | *.exe 36 | *.out 37 | *.app 38 | *.i*86 39 | *.x86_64 40 | *.hex 41 | 42 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at proxy@ryan6578.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Codewords 2 | This project is a LUA script written for [Tabletop Simulator](https://www.tabletopsimulator.com). Please note that this repository only contains the source code for the project and does not include the corresponding assets. 3 | 4 | ## Getting Started 5 | If you'd like to help contribute to the project, please ensure you have the following prerequisites: 6 | 7 | * [Tabletop Simulator Game](https://store.steampowered.com/app/286160/Tabletop_Simulator/) 8 | * [Atom](https://atom.io/) 9 | * [TTS Atom plugin](https://github.com/Berserk-Games/atom-tabletopsimulator-lua/wiki/Installation) 10 | 11 | Once you have everything set up, you can get started working on the project via the following guidelines: 12 | 1. Subscribe to the [Codewords workshop mod](https://steamcommunity.com/sharedfiles/filedetails/?id=1370354402). 13 | 2. Start Tabletop Simulator. 14 | 3. Open up Atom (with the TTS plugin already installed). 15 | 4. Create a new server. 16 | 5. Load the workshop mod (Games > Workshop > Codewords). 17 | 6. Open up Atom. The script should already be loaded in the editor. If not, use the `CTRL` + `SHIFT` + `L` key combination to get LUA scripts from the game. 18 | 7. Clone the repository and copy the contents of the files from the repository into their respectively named files on your local instance. 19 | 20 | ## Help 21 | Feel free to join our [Discord](https://discord.gg/m4qspZE) if you need help with getting started on the mod (or would just like to join the Codewords community on TTS). Hope to see you there! :) 22 | -------------------------------------------------------------------------------- /src/Custom Assetbundle.0a61c6.ttslua: -------------------------------------------------------------------------------- 1 | -- UI elements for the table 2 | 3 | -- Tracks the players currently in queue 4 | queue = {} 5 | 6 | -- Tracks color switching 7 | seatTracker = 8 | { 9 | ["Blue"] = false, 10 | ["Teal"] = false, 11 | ["Purple"] = false, 12 | ["Green"] = false, 13 | ["White"] = false, 14 | 15 | ["Red"] = false, 16 | ["Orange"] = false, 17 | ["Yellow"] = false, 18 | ["Pink"] = false, 19 | ["Brown"] = false 20 | } 21 | 22 | redColor = {0.856, 0.1, 0.094} 23 | 24 | function onLoad(saveState) 25 | if saveState != "" then 26 | local decodedSaveState = JSON.decode(saveState) 27 | 28 | queue = decodedSaveState.queue 29 | updateQueue() 30 | end 31 | 32 | -- Update the tracker with vacant seats and show buttons for available seats 33 | for seatColor,_ in pairs(seatTracker) do 34 | if Player[seatColor].seated then 35 | seatTracker[seatColor] = true 36 | else 37 | seatTracker[seatColor] = false 38 | 39 | -- Show the button to switch seats 40 | self.UI.setAttribute("sit" .. seatColor, "active", true) 41 | end 42 | end 43 | end 44 | 45 | function onSave() 46 | local saveData = {} 47 | 48 | saveData.queue = queue 49 | 50 | return JSON.encode(saveData) 51 | end 52 | 53 | function previousPage(player, button) 54 | if player.color == "Grey" or button ~= "-1" then 55 | return 56 | end 57 | 58 | Global.call("previousPage") 59 | end 60 | 61 | function nextPage(player, button) 62 | if player.color == "Grey" or button ~= "-1" then 63 | return 64 | end 65 | 66 | Global.call("nextPage") 67 | end 68 | 69 | function searchDecks(player, searchTerm) 70 | if player.color == "Grey" then 71 | return 72 | end 73 | 74 | self.UI.setAttribute("deckSearch", "text", searchTerm) 75 | 76 | Global.call("searchDecks", searchTerm) 77 | end 78 | 79 | function votePass(player) 80 | Global.call("votePass", player.color) 81 | end 82 | 83 | function onPlayerChangeColor(color) 84 | local switcherEnabled = Global.call("getSwitcher") 85 | 86 | if not switcherEnabled then 87 | return 88 | end 89 | 90 | for seatColor,_ in pairs(seatTracker) do 91 | if Player[seatColor].seated != seatTracker[seatColor] then 92 | seatTracker[seatColor] = Player[seatColor].seated 93 | self.UI.setAttribute("sit" .. seatColor, "active", not seatTracker[seatColor]) 94 | end 95 | end 96 | end 97 | 98 | function onPlayerConnect(player) 99 | for _,queuePlayer in ipairs(queue) do 100 | if queuePlayer.steam_id == player.steam_id then 101 | queuePlayer.steam_name = player.steam_name 102 | updateQueue() 103 | break 104 | end 105 | end 106 | end 107 | 108 | function updateSettings() 109 | shuffle(nil, Global.call("getShuffle") == true and "True" or "False") 110 | queues(nil, Global.call("getQueue") == true and "True" or "False") 111 | switcher(nil, Global.call("getSwitcher") == true and "True" or "False") 112 | tilting(nil, Global.call("getTilting") == true and "True" or "False") 113 | --afk(player) 114 | --afkTime(player) 115 | timer(nil, Global.call("getTimer") == true and "True" or "False") 116 | timer1Time(nil, Global.call("getTimer1Time")) 117 | timer2Time(nil, Global.call("getTimer2Time")) 118 | autokick(nil, Global.call("getAutokick") == true and "True" or "False") 119 | end 120 | 121 | function reloadUI(player) 122 | self.UI.setXml(self.UI.getXml()) 123 | Global.call("reloadCardUI") 124 | printToAll(player.steam_name .. " has reloaded the UI!", redColor) 125 | end 126 | 127 | --------------------------------------------------------------- 128 | ---------------------------[ TIMER ]--------------------------- 129 | --------------------------------------------------------------- 130 | function startTimer(time) 131 | -- Cancel any existing timers 132 | if currentTimer then 133 | stopTimer(false) 134 | end 135 | 136 | local minutes = math.floor(time / 60) 137 | local seconds = time % 60 138 | 139 | timeLeft = time 140 | 141 | local timerColor = "White" 142 | if timeLeft <= 10 then 143 | timerColor = "Red" 144 | broadcastToAll("[a020f0]» [ffffff]" .. (timeLeft == 0 and "Time's up!" or (tostring(timeLeft) .. " seconds remaining!")) .. " [a020f0]«") 145 | elseif timeLeft <= 30 then 146 | timerColor = "Yellow" 147 | end 148 | 149 | -- Set the correct time on the clock 150 | self.UI.setAttributes("time", { 151 | text = tostring(minutes) .. ":" .. (seconds < 10 and ("0" .. tostring(seconds)) or tostring(seconds)), 152 | color = timerColor 153 | }) 154 | 155 | -- Set the toggle button to the correct text 156 | self.UI.setAttributes("timerControl", { 157 | text = "ll", 158 | fontStyle = "Bold" 159 | }) 160 | 161 | -- Start the countdown 162 | currentTimer = Wait.time(tickTimer, 1, time) 163 | end 164 | 165 | function tickTimer() 166 | -- Tick the timer down by 1 second 167 | timeLeft = timeLeft - 1 168 | 169 | local minutes = math.floor(timeLeft / 60) 170 | local seconds = timeLeft % 60 171 | 172 | local timerColor = "White" 173 | if timeLeft <= 10 then 174 | timerColor = "Red" 175 | 176 | -- Play the ticking sound 177 | self.AssetBundle.playTriggerEffect(3) 178 | 179 | broadcastToAll("[a020f0]» [ffffff]" .. (timeLeft == 0 and "Time's up!" or (tostring(timeLeft) .. " seconds remaining!")) .. " [a020f0]«") 180 | elseif timeLeft <= 30 then 181 | timerColor = "Yellow" 182 | end 183 | 184 | -- Set the correct time on the clock 185 | self.UI.setAttributes("time", { 186 | text = tostring(minutes) .. ":" .. (seconds < 10 and ("0" .. tostring(seconds)) or tostring(seconds)), 187 | color = timerColor 188 | }) 189 | 190 | -- Swap turns if time expired 191 | if timeLeft == 0 then 192 | Global.call("timeExpired") 193 | end 194 | 195 | end 196 | 197 | function toggleTimer(player, button) 198 | if player.color == "Grey" or button ~= "-1" then 199 | return 200 | end 201 | 202 | if player.color ~= "Red" and player.color ~= "Blue" and not player.admin then 203 | player.broadcast("[a020f0]» [da1918]ERROR: [ffffff]Only Codemasters or promoted players may control the timer! [a020f0]«") 204 | return 205 | end 206 | 207 | -- Pause/start the current timer if it exists 208 | if currentTimer then 209 | -- Pause the current timer 210 | stopTimer(false) 211 | 212 | -- Change the toggle button text 213 | self.UI.setAttributes("timerControl", { 214 | text = "▶", 215 | fontStyle = "Normal" 216 | }) 217 | else 218 | -- Start the previous timer (if one existed) 219 | if timeLeft and (timeLeft > 0) then 220 | startTimer(timeLeft) 221 | 222 | -- Change the toggle button text 223 | self.UI.setAttributes("timerControl", { 224 | text = "ll", 225 | fontStyle = "Bold" 226 | }) 227 | end 228 | end 229 | 230 | end 231 | 232 | function stopTimer(endGame) 233 | if currentTimer then 234 | Wait.stop(currentTimer) 235 | currentTimer = nil 236 | end 237 | 238 | if endGame then 239 | timeLeft = nil 240 | 241 | -- Set the correct time on the clock 242 | self.UI.setAttributes("time", { 243 | text = "0:00", 244 | color = "Red" 245 | }) 246 | 247 | -- Set the toggle button to the correct text 248 | self.UI.setAttributes("timerControl", { 249 | text = "▶", 250 | fontStyle = "Normal" 251 | }) 252 | end 253 | end 254 | 255 | --------------------------------------------------------------- 256 | --------------------[ GAME SETTINGS PANEL ]-------------------- 257 | --------------------------------------------------------------- 258 | function shuffle(player, enabled) 259 | if player ~= nil and not player.admin then 260 | self.UI.setAttribute("shuffle", "isOn", self.UI.getAttribute("shuffle", "isOn")) 261 | player.broadcast("[a020f0]» [da1918]ERROR: [ffffff]Only promoted players may change game settings! [a020f0]«") 262 | return 263 | end 264 | 265 | -- Handle in the Global script 266 | Global.call("setShuffle", toboolean(enabled)) 267 | 268 | -- Update the view for everyone 269 | self.UI.setAttribute("shuffle", "isOn", toboolean(enabled)) 270 | end 271 | 272 | function queues(player, enabled) 273 | if player ~= nil and not player.admin then 274 | self.UI.setAttribute("queues", "isOn", self.UI.getAttribute("queues", "isOn")) 275 | player.broadcast("[a020f0]» [da1918]ERROR: [ffffff]Only promoted players may change game settings! [a020f0]«") 276 | return 277 | end 278 | 279 | -- Handle in the Global script 280 | Global.call("setQueue", toboolean(enabled)) 281 | 282 | -- Hide/show the queue 283 | self.UI.setAttribute("codemasterQueue", "active", toboolean(enabled)) 284 | if not enabled then 285 | queue = {} 286 | end 287 | updateQueue() 288 | 289 | -- Update the view for everyone 290 | self.UI.setAttribute("queues", "isOn", toboolean(enabled)) 291 | end 292 | 293 | function switcher(player, enabled) 294 | if player ~= nil and not player.admin then 295 | self.UI.setAttribute("switcher", "isOn", self.UI.getAttribute("switcher", "isOn")) 296 | player.broadcast("[a020f0]» [da1918]ERROR: [ffffff]Only promoted players may change game settings! [a020f0]«") 297 | return 298 | end 299 | 300 | -- Handle in the Global script 301 | Global.call("setSwitcher", toboolean(enabled)) 302 | 303 | local colors = 304 | { 305 | "Blue", 306 | "Red", 307 | "Teal", 308 | "Orange", 309 | "Purple", 310 | "Yellow", 311 | "Green", 312 | "Pink", 313 | "White", 314 | "Brown" 315 | } 316 | for _, color in ipairs(colors) do 317 | if toboolean(enabled) then 318 | if Player[color].seated then 319 | self.UI.setAttribute("sit" .. color, "active", false) 320 | else 321 | self.UI.setAttribute("sit" .. color, "active", toboolean(enabled)) 322 | end 323 | else 324 | self.UI.setAttribute("sit" .. color, "active", toboolean(enabled)) 325 | end 326 | end 327 | 328 | -- Update the view for everyone 329 | self.UI.setAttribute("switcher", "isOn", toboolean(enabled)) 330 | end 331 | 332 | function tilting(player, enabled) 333 | if player ~= nil and not player.admin then 334 | self.UI.setAttribute("tilting", "isOn", self.UI.getAttribute("tilting", "isOn")) 335 | player.broadcast("[a020f0]» [da1918]ERROR: [ffffff]Only promoted players may change game settings! [a020f0]«") 336 | return 337 | end 338 | 339 | -- Handle in the Global script 340 | Global.call("setTilting", toboolean(enabled)) 341 | 342 | -- Update the view for everyone 343 | self.UI.setAttribute("tilting", "isOn", toboolean(enabled)) 344 | end 345 | 346 | function afk(player) 347 | if player ~= nil and not player.admin then 348 | self.UI.setAttribute("afk", "isOn", self.UI.getAttribute("afk", "isOn")) 349 | player.broadcast("[a020f0]» [da1918]ERROR: [ffffff]Only promoted players may change game settings! [a020f0]«") 350 | return 351 | end 352 | 353 | -- Handle in the Global script 354 | Global.call("setAfk", toboolean(enabled)) 355 | 356 | -- Update the promoted player view 357 | self.UI.setAttribute("afk", "isOn", toboolean(enabled)) 358 | end 359 | 360 | function afkTime(player) 361 | if player ~= nil and not player.admin then 362 | return 363 | end 364 | end 365 | 366 | function timer(player, enabled) 367 | if player ~= nil and not player.admin then 368 | self.UI.setAttribute("timer", "isOn", self.UI.getAttribute("timer", "isOn")) 369 | player.broadcast("[a020f0]» [da1918]ERROR: [ffffff]Only promoted players may change game settings! [a020f0]«") 370 | return 371 | end 372 | 373 | -- Handle in the Global script 374 | Global.call("setTimer", toboolean(enabled)) 375 | 376 | -- Update the view for everyone 377 | self.UI.setAttribute("timer", "isOn", toboolean(enabled)) 378 | end 379 | 380 | function timer1Time(player, value) 381 | if player ~= nil and not player.admin then 382 | self.UI.setAttribute("timer1Time", "value", self.UI.getAttribute("timer1Time", "value")) 383 | player.broadcast("[a020f0]» [da1918]ERROR: [ffffff]Only promoted players may change game settings! [a020f0]«") 384 | return 385 | end 386 | 387 | -- Handle in the Global script 388 | Global.call("setTimer1Time", tonumber(value)) 389 | 390 | -- Update the view for everyone 391 | self.UI.setAttribute("timer1Time", "value", tonumber(value)) 392 | self.UI.setAttribute("timer1TimeValue", "text", value) 393 | end 394 | 395 | function timer2Time(player, value) 396 | if player ~= nil and not player.admin then 397 | self.UI.setAttribute("timer2Time", "value", self.UI.getAttribute("timer2Time", "value")) 398 | player.broadcast("[a020f0]» [da1918]ERROR: [ffffff]Only promoted players may change game settings! [a020f0]«") 399 | return 400 | end 401 | 402 | -- Handle in the Global script 403 | Global.call("setTimer2Time", tonumber(value)) 404 | 405 | -- Update the view for everyone 406 | self.UI.setAttribute("timer2Time", "value", tonumber(value)) 407 | self.UI.setAttribute("timer2TimeValue", "text", value) 408 | end 409 | 410 | function autokick(player, enabled) 411 | if player ~= nil and not player.admin then 412 | self.UI.setAttribute("autokick", "isOn", self.UI.getAttribute("autokick", "isOn")) 413 | player.broadcast("[a020f0]» [da1918]ERROR: [ffffff]Only promoted players may change game settings! [a020f0]«") 414 | return 415 | end 416 | 417 | -- Handle in the Global script 418 | Global.call("setAutokick", toboolean(enabled)) 419 | 420 | -- Update the view for everyone 421 | self.UI.setAttribute("autokick", "isOn", toboolean(enabled)) 422 | end 423 | 424 | 425 | --------------------------------------------------------------- 426 | --------------------[ DECK SELECTOR PANEL ]-------------------- 427 | --------------------------------------------------------------- 428 | function setDeck(player, deck, button) 429 | if player.color == "Grey" then 430 | return 431 | end 432 | 433 | if player.color == "Red" or player.color == "Blue" or player.admin then 434 | Global.call("setDeck", deck) 435 | 436 | local multipleDeckMode = Global.call("getMultipleDeckMode") 437 | for i = 1, 20, 1 do 438 | local buttonName = "deck" .. i .. "Button" 439 | if buttonName == button then 440 | local currentColor = self.UI.getAttribute(buttonName, "color") 441 | if multipleDeckMode and currentColor == "#aaeaa7" then 442 | self.UI.setAttribute(buttonName, "color", "#ffffff") 443 | else 444 | self.UI.setAttribute(buttonName, "color", "#aaeaa7") 445 | end 446 | else 447 | if not multipleDeckMode then 448 | self.UI.setAttribute(buttonName, "color", "#ffffff") 449 | end 450 | end 451 | end 452 | else 453 | player.broadcast("[a020f0]» [da1918]ERROR: [ffffff]Only Codemasters or promoted players may select a deck! [a020f0]«") 454 | end 455 | end 456 | 457 | function startGame(player, button) 458 | if player.color == "Grey" or button ~= "-1" then 459 | return 460 | end 461 | 462 | if player.color == "Red" or player.color == "Blue" or player.admin then 463 | Global.call("startGame", player) 464 | else 465 | player.broadcast("[a020f0]» [da1918]ERROR: [ffffff]Only Codemasters or promoted players may start the game! [a020f0]«") 466 | end 467 | end 468 | 469 | function refreshDecks(player, button) 470 | if player.color == "Grey" or button ~= "-1" then 471 | return 472 | end 473 | 474 | Global.call("api_getDecks", player) 475 | player.broadcast("[a020f0]» [ffffff]Deck list refreshed! [a020f0]«", redColor) 476 | end 477 | 478 | function deckMode(player, button) 479 | if player.color == "Grey" or button ~= "-1" then 480 | return 481 | end 482 | 483 | if player.color == "Red" or player.color == "Blue" or player.admin then 484 | Global.call("toggleDeckMode", player) 485 | else 486 | player.broadcast("[a020f0]» [da1918]ERROR: [ffffff]Only Codemasters or promoted players may change the deck mode! [a020f0]«") 487 | end 488 | 489 | local multipleDeckMode = Global.call("getMultipleDeckMode") 490 | 491 | if not multipleDeckMode then 492 | Global.call("setDeck", nil) 493 | for i = 1, 20, 1 do 494 | local buttonName = "deck" .. i .. "Button" 495 | self.UI.setAttribute(buttonName, "color", "#ffffff") 496 | end 497 | end 498 | 499 | if multipleDeckMode then 500 | self.UI.setAttribute("deckModeSwitch", "color", "#96e592") 501 | player.broadcast("[a020f0]» [ffffff]Multiple deck mode: [31b32b]ENABLED [a020f0]«") 502 | else 503 | self.UI.setAttribute("deckModeSwitch", "color", "#ffffff") 504 | player.broadcast("[a020f0]» [ffffff]Multiple deck mode: [da1918]DISABLED [a020f0]«") 505 | end 506 | end 507 | 508 | 509 | ------------------------------------------------------------------ 510 | --------------------[ CODEMASTER QUEUE PANEL ]-------------------- 511 | ------------------------------------------------------------------ 512 | function join(player, button) 513 | if button ~= "-1" then 514 | return 515 | end 516 | 517 | if player.color == "Grey" or player.color == "Black" then 518 | player.broadcast("[a020f0]» [da1918]ERROR: [ffffff]Only seated players may join the queue! [a020f0]«") 519 | return 520 | end 521 | 522 | -- Check to see if the player already exists in the queue 523 | for _,queuePlayer in ipairs(queue) do 524 | if queuePlayer.steam_id == player.steam_id then 525 | if queuePlayer.stay then 526 | queuePlayer.stay = false 527 | updateQueue() 528 | end 529 | return 530 | end 531 | end 532 | 533 | if #queue >= 10 then 534 | player.broadcast("[a020f0]» [da1918]ERROR: [ffffff]Codemaster queue is full. Please wait until someone leaves or is removed! [a020f0]«") 535 | return 536 | end 537 | 538 | -- Join the player to the queue and make them stay 539 | table.insert(queue, { 540 | steam_id = player.steam_id, 541 | steam_name = player.steam_name, 542 | stay = false 543 | }) 544 | updateQueue() 545 | end 546 | 547 | function stay(player, button) 548 | if button ~= "-1" then 549 | return 550 | end 551 | 552 | if player.color == "Grey" or player.color == "Black" then 553 | player.broadcast("[a020f0]» [da1918]ERROR: [ffffff]Only seated players may join the queue! [a020f0]«") 554 | return 555 | end 556 | 557 | -- Check to see if the player already exists in the queue 558 | for _,queuePlayer in ipairs(queue) do 559 | if queuePlayer.steam_id == player.steam_id then 560 | if not queuePlayer.stay then 561 | queuePlayer.stay = true 562 | updateQueue() 563 | end 564 | return 565 | end 566 | end 567 | 568 | if #queue >= 10 then 569 | player.broadcast("[a020f0]» [da1918]ERROR: [ffffff]Codemaster queue is full. Please wait until someone leaves or is removed! [a020f0]«") 570 | return 571 | end 572 | 573 | -- Join the player to the queue and make them stay 574 | table.insert(queue, { 575 | steam_id = player.steam_id, 576 | steam_name = player.steam_name, 577 | stay = true 578 | }) 579 | updateQueue() 580 | end 581 | 582 | function leave(player, button) 583 | if button ~= "-1" then 584 | return 585 | end 586 | 587 | if player.color == "Grey" or player.color == "Black" then 588 | player.broadcast("[a020f0]» [da1918]ERROR: [ffffff]Only seated players may leave the queue! [a020f0]«") 589 | return 590 | end 591 | 592 | -- Check to see if the player already exists in the queue 593 | for i, queuePlayer in ipairs(queue) do 594 | if queuePlayer.steam_id == player.steam_id then 595 | table.remove(queue, i) 596 | updateQueue() 597 | return 598 | end 599 | end 600 | end 601 | 602 | function remove(player, queuePosition) 603 | if player ~= nil and not player.admin then 604 | player.broadcast("[a020f0]» [da1918]ERROR: [ffffff]Only promoted players may remove player from the queue! [a020f0]«") 605 | return 606 | end 607 | 608 | -- Remove this player from the queue 609 | local removedPlayer = findPlayerBySteamID(table.remove(queue, queuePosition).steam_id) 610 | updateQueue() 611 | 612 | if removedPlayer ~= nil then 613 | -- Notify the player they've been removed 614 | removedPlayer.broadcast("[a020f0]» [ffffff]" .. player.steam_name .. " has removed you from the codemaster queue! [a020f0]«") 615 | end 616 | end 617 | 618 | 619 | ---------------------------------------------------------- 620 | --------------------[ COLOR SWITCHER ]-------------------- 621 | ---------------------------------------------------------- 622 | function changeSeat(player, to) 623 | if player.color == "Grey" or player.color == "Black" then 624 | return 625 | end 626 | 627 | -- Ensure the seat is empty 628 | if Player[to].seated then 629 | player.broadcast("[a020f0]» [da1918]ERROR: [ffffff]" .. to .. " seat is not empty! [a020f0]«") 630 | return 631 | end 632 | 633 | player.changeColor(to) 634 | end 635 | 636 | 637 | ------------------------------------------------------------- 638 | --------------------[ UTILITY FUNCTIONS ]-------------------- 639 | ------------------------------------------------------------- 640 | function toboolean(string) 641 | if string == "True" then 642 | return true 643 | elseif string == "False" then 644 | return false 645 | else 646 | return nil 647 | end 648 | end 649 | 650 | ---------------------------------------------------------------------------------------------------------------------- 651 | function updateQueue() 652 | for i=1,10,1 do 653 | if queue[i] ~= nil then 654 | self.UI.setAttribute("queueText" .. i, "text", queue[i].steam_name) 655 | self.UI.setAttributes("queueButton" .. i, { 656 | active = true, 657 | color = queue[i].stay and "#96e592" or "White" 658 | }) 659 | else 660 | self.UI.setAttributes("queueButton" .. i, { 661 | active = false, 662 | color = "White" 663 | }) 664 | self.UI.setAttribute("queueText" .. i, "text", "") 665 | end 666 | end 667 | end 668 | 669 | function shufflePlayers() 670 | 671 | -- Whether or not the codemaster queue is enabled 672 | local queueEnabled = Global.call("getQueue") 673 | 674 | -- Colors to use when sitting people 675 | local colorPool = 676 | { 677 | "Blue", 678 | "Red", 679 | "Teal", 680 | "Orange", 681 | "Purple", 682 | "Yellow", 683 | "Green", 684 | "Pink", 685 | "White", 686 | "Brown" 687 | } 688 | local colors = {} 689 | 690 | -- Get all current seated players to shuffle and stand them 691 | local seatedPlayers = {} 692 | for _, player in ipairs(Player.getPlayers()) do 693 | if player.color ~= "Grey" and player.color ~= "Black" then 694 | table.insert(seatedPlayers, player) 695 | player.changeColor("Grey") 696 | coroutine.yield(0) 697 | 698 | -- Add a color to the list of colors 699 | table.insert(colors, (#seatedPlayers % 2 == 0) and table.remove(colorPool, 1) or table.remove(colorPool, math.random(1, 2))) 700 | end 701 | end 702 | 703 | local requeue = {} 704 | local numCodemasters = 0 705 | 706 | while #seatedPlayers > 0 do 707 | local nextPlayer 708 | if queueEnabled and numCodemasters < 2 and #queue > 0 then 709 | -- Draw from the queue first 710 | local nextInQueue 711 | while #queue > 0 do 712 | nextInQueue = table.remove(queue, 1) 713 | nextPlayer = findPlayerBySteamID(nextInQueue.steam_id) 714 | 715 | if nextPlayer ~= nil then 716 | -- Check to see they were seated 717 | local seated = false 718 | for i, player in ipairs(seatedPlayers) do 719 | if player.steam_id == nextPlayer.steam_id then 720 | seated = true 721 | table.remove(seatedPlayers, i) 722 | break 723 | end 724 | end 725 | 726 | if seated and not nextPlayer.blindfolded then 727 | if nextInQueue.stay then 728 | table.insert(requeue, nextPlayer) 729 | end 730 | -- We found a valid player - break 731 | numCodemasters = numCodemasters + 1 732 | break 733 | else 734 | nextPlayer.broadcast("[a020f0]» [ffffff]You were removed from the codemaster queue because you were either not sitting for your turn or were AFK! [a020f0]«") 735 | end 736 | end 737 | end 738 | elseif #seatedPlayers > 0 then 739 | nextPlayer = table.remove(seatedPlayers, math.random(1, #seatedPlayers)) 740 | else 741 | -- No more players to seat 742 | break 743 | end 744 | 745 | -- Seat the next player 746 | nextPlayer.changeColor(table.remove(colors, 1)) 747 | coroutine.yield(0) 748 | end 749 | 750 | -- Requeue players if unnecessary 751 | for _, requeuePlayer in ipairs(requeue) do 752 | table.insert(queue, { 753 | steam_id = requeuePlayer.steam_id, 754 | steam_name = requeuePlayer.steam_name, 755 | stay = true 756 | }) 757 | end 758 | updateQueue() 759 | Global.call("api_gameStart") 760 | return 1 761 | end 762 | 763 | function swapCodemasters() 764 | 765 | local colorPool = 766 | { 767 | "Blue", 768 | "Red" 769 | } 770 | 771 | local requeue = {} 772 | 773 | local nextInQueue 774 | while #queue > 0 and #colorPool > 0 do 775 | nextInQueue = table.remove(queue, 1) 776 | nextPlayer = findPlayerBySteamID(nextInQueue.steam_id) 777 | 778 | if nextPlayer ~= nil then 779 | -- Check to see they were seated 780 | if nextPlayer.color != "Grey" and nextPlayer.color != "Black" and not nextPlayer.blindfolded then 781 | if nextInQueue.stay then 782 | table.insert(requeue, nextPlayer) 783 | end 784 | -- We found a valid player - break 785 | local codemasterSeat = table.remove(colorPool, math.random(1, #colorPool)) 786 | local oldCodemaster = Player[codemasterSeat] 787 | if oldCodemaster.seated then 788 | local switchColor = nextPlayer.color 789 | nextPlayer.changeColor("Grey") 790 | coroutine.yield(0) 791 | oldCodemaster.changeColor(switchColor) 792 | coroutine.yield(0) 793 | end 794 | nextPlayer.changeColor(codemasterSeat) 795 | coroutine.yield(0) 796 | else 797 | nextPlayer.broadcast("[a020f0]» [ffffff]You were removed from the codemaster queue because you were either not sitting for your turn or were AFK! [a020f0]«") 798 | end 799 | end 800 | end 801 | 802 | -- Requeue players if unnecessary 803 | for _, requeuePlayer in ipairs(requeue) do 804 | table.insert(queue, { 805 | steam_id = requeuePlayer.steam_id, 806 | steam_name = requeuePlayer.steam_name, 807 | stay = true 808 | }) 809 | end 810 | updateQueue() 811 | Global.call("api_gameStart") 812 | return 1 813 | end 814 | 815 | function findPlayerBySteamID(steam_id) 816 | for _, player in ipairs(Player.getPlayers()) do 817 | if player.steam_id == steam_id then 818 | return player 819 | end 820 | end 821 | return nil 822 | end -------------------------------------------------------------------------------- /src/Custom Assetbundle.0a61c6.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 11 | 16 | 17 | 18 | 26 | 27 | 28 | 29 | 30 | 35 | 36 | 37 | 41 | 42 | 46 | 47 | 48 | 55 | 56 | 57 | 63 | 64 | 65 | 71 | 72 | 73 | 77 | 78 | 79 | 83 | 84 | 85 | 90 | 91 | 92 | 97 | 98 | 99 | 104 | 105 | 106 | 107 | 108 | 111 | 112 | 113 | 118 | 119 | 120 | 123 | 124 | 125 | 129 | 130 | 131 | 134 | 135 | 136 | 140 | 141 | 142 | 149 | 150 | 151 | 155 | 156 | 157 | 158 | 159 | 164 | 165 | 168 | 169 | 170 | 171 | 172 | 178 | 179 | 180 | 181 | 182 | 189 | 190 | 196 | 197 | 198 | 199 | 200 | 214 | 215 | 224 | 225 | 234 | 235 | 244 | 245 | 254 | 255 | 264 | 265 | 274 | 275 | 284 | 285 | 294 | 295 | 304 | 305 | 314 | 315 | 316 | 317 | 321 | 322 | 323 | 324 | Deck Selector 325 | 326 | 327 | 328 | Page: 0 / 0 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 358 | 359 | 360 | 361 | 383 | 384 | 385 | 386 | 408 | 409 | 410 | 411 | 433 | 434 | 435 | 436 | 458 | 459 | 460 | 461 | 483 | 484 | 485 | 486 | 508 | 509 | 510 | 511 | 533 | 534 | 535 | 536 | 558 | 559 | 560 | 561 | 583 | 584 | 585 | 586 | 608 | 609 | 610 | 611 | 633 | 634 | 635 | 636 | 658 | 659 | 660 | 661 | 683 | 684 | 685 | 686 | 708 | 709 | 710 | 711 | 733 | 734 | 735 | 736 | 758 | 759 | 760 | 761 | 783 | 784 | 785 | 786 | 808 | 809 | 810 | 811 | 833 | 834 | 835 | 836 | 837 | 838 | 839 | 840 | 841 | 842 | 843 | 844 | 845 | 846 | 847 | 850 | 851 | 852 | 853 | Game Settings 854 | 855 | 856 | 857 | 858 | 859 | General Settings 860 | 861 | 862 | 863 | Shuffle Players at Start 864 | 865 | 866 | 867 | 868 | Enable Codemaster Queue 869 | 870 | 871 | 872 | 873 | Enable Quick Color Switching 874 | 875 | 876 | 877 | 878 | Disable Tilting Before Clue Given 879 | 880 | 881 | 882 | 883 | AFK Settings 884 | 885 | 886 | 887 | AFK Detection 888 | 889 | 890 | 891 | 892 | AFK Minutes 893 | 894 | 895 | 896 | 897 | 898 | 899 | 900 | 0 901 | 902 | 903 | 904 | 905 | 906 | 907 | 908 | Time Settings 909 | 910 | 911 | 912 | Timer Enabled 913 | 914 | 915 | 916 | 917 | Timer (round 1) 918 | 919 | 920 | 921 | 922 | 923 | 924 | 925 | 8 926 | 927 | 928 | 929 | 930 | 931 | 932 | 933 | Timer (round 2+) 934 | 935 | 936 | 937 | 938 | 939 | 940 | 941 | 4 942 | 943 | 944 | 945 | 946 | 947 | 948 | 949 | Automod Settings 950 | 951 | 952 | 953 | Autokick Players on Blacklist 954 | 955 | 956 | 957 | 958 | 959 | 960 | 961 | 962 | 963 | 964 | 968 | 969 | 970 | 971 | Codemaster Queue 972 | 973 | 974 | 975 | 976 | 977 | 980 | 981 | 982 | 983 | 986 | 987 | 988 | 989 | 992 | 993 | 994 | 995 | 998 | 999 | 1000 | 1001 | 1004 | 1005 | 1006 | 1007 | 1010 | 1011 | 1012 | 1013 | 1016 | 1017 | 1018 | 1019 | 1022 | 1023 | 1024 | 1025 | 1028 | 1029 | 1030 | 1031 | 1034 | 1035 | 1036 | 1037 | 1038 | 1039 | 1040 | 1041 | 1042 | 1043 | 1044 | 1045 | 1046 | 1047 | 1048 | 1052 | 1053 | 1054 | 1055 | ──── Time Remaining ──── 1056 | 1057 | 1058 | 0:00 1059 | 1060 | 1061 | 1062 | 1063 | 1064 | 1065 | 1066 | 1067 | 1068 | 1069 | 1070 | 1071 | 1072 | 1073 | 1074 | 1075 | 1076 | 1077 | 1078 | 1079 | 1080 | 1081 | 1082 | 1083 | 1084 | 1085 | 1086 | 1087 | 1088 | 1089 | 1090 | 1091 | 1092 | 1093 | 1094 | 1095 | 1096 | 1097 | 1098 | 1099 | 1100 | 1101 | 1102 | 1103 | 1104 | 1105 | 1106 | 1107 | 1108 | 1109 | 1110 | 1111 | 1112 | 1113 | 1114 | 1115 | 1116 | 1117 | 1118 | 1119 | 1120 | 1121 | 1122 | 1123 | 1124 | 1125 | 1126 | 1127 | 1128 | 1129 | 1130 | 1131 | 1132 | 1133 | Version 3.2.3-beta (git-BETA) 1134 | Made with by Rob Ford 1135 | TTS-CODENAMES.COM 1136 | Check out the brand new website above to browse through and create/edit your own custom TTS-Codenames decks! -------------------------------------------------------------------------------- /src/Custom Assetbundle.3f75b3.ttslua: -------------------------------------------------------------------------------- 1 | function clue() 2 | end -------------------------------------------------------------------------------- /src/Custom Assetbundle.f3cdf2.ttslua: -------------------------------------------------------------------------------- 1 | function clue() 2 | end -------------------------------------------------------------------------------- /src/Global.-1.ttslua: -------------------------------------------------------------------------------- 1 | --[[ 2 | TTS-Codenames: A LUA script for Codenames on Tabletop Simulator for Steam. 3 | Copyright (C) 2021 Ryan6578 (https://ryan6578.com) 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | ]] 18 | 19 | chatDisclaimer = 20 | [[ 21 | 22 | [da1918]ANALYTICS DISCLAIMER[-] 23 | [-]This mod collects certain data to improve the 24 | experience and provide meaningful insight 25 | into usage and play. By continuing, you 26 | acknowledge and accept this in accordance 27 | with the privacy policy outlined below. 28 | 29 | More info: 30 | [8bb5ff]https://tts-codenames.com/privacy[-] 31 | 32 | ]] 33 | 34 | redColor = {0.856, 0.1, 0.094} 35 | blueColor = {0.118, 0.53, 1} 36 | 37 | -- Color = Blue/Red/Black/White 38 | cards = { 39 | -- Row 1 (cards 1-5) 40 | {position = {x = -11.4, z = 12.6}, guid = nil, id = nil, value = nil, color = nil, covered = false}, 41 | {position = {x = -5.7, z = 12.6}, guid = nil, id = nil, value = nil, color = nil, covered = false}, 42 | {position = {x = 0, z = 12.6}, guid = nil, id = nil, value = nil, color = nil, covered = false}, 43 | {position = {x = 5.7, z = 12.6}, guid = nil, id = nil, value = nil, color = nil, covered = false}, 44 | {position = {x = 11.4, z = 12.6}, guid = nil, id = nil, value = nil, color = nil, covered = false}, 45 | 46 | -- Row 2 (cards 6-10) 47 | {position = {x = -11.4, z = 8.75}, guid = nil, id = nil, value = nil, color = nil, covered = false}, 48 | {position = {x = -5.7, z = 8.75}, guid = nil, id = nil, value = nil, color = nil, covered = false}, 49 | {position = {x = 0, z = 8.75}, guid = nil, id = nil, value = nil, color = nil, covered = false}, 50 | {position = {x = 5.7, z = 8.75}, guid = nil, id = nil, value = nil, color = nil, covered = false}, 51 | {position = {x = 11.4, z = 8.75}, guid = nil, id = nil, value = nil, color = nil, covered = false}, 52 | 53 | -- Row 3 (cards 11-15) 54 | {position = {x = -11.4, z = 4.9}, guid = nil, id = nil, value = nil, color = nil, covered = false}, 55 | {position = {x = -5.7, z = 4.9}, guid = nil, id = nil, value = nil, color = nil, covered = false}, 56 | {position = {x = 0, z = 4.9}, guid = nil, id = nil, value = nil, color = nil, covered = false}, 57 | {position = {x = 5.7, z = 4.9}, guid = nil, id = nil, value = nil, color = nil, covered = false}, 58 | {position = {x = 11.4, z = 4.9}, guid = nil, id = nil, value = nil, color = nil, covered = false}, 59 | 60 | -- Row 4 (cards 16-20) 61 | {position = {x = -11.4, z = 1.05}, guid = nil, id = nil, value = nil, color = nil, covered = false}, 62 | {position = {x = -5.7, z = 1.05}, guid = nil, id = nil, value = nil, color = nil, covered = false}, 63 | {position = {x = 0, z = 1.05}, guid = nil, id = nil, value = nil, color = nil, covered = false}, 64 | {position = {x = 5.7, z = 1.05}, guid = nil, id = nil, value = nil, color = nil, covered = false}, 65 | {position = {x = 11.4, z = 1.05}, guid = nil, id = nil, value = nil, color = nil, covered = false}, 66 | 67 | -- Row 5 (cards 21-25) 68 | {position = {x = -11.4, z = -2.8}, guid = nil, id = nil, value = nil, color = nil, covered = false}, 69 | {position = {x = -5.7, z = -2.8}, guid = nil, id = nil, value = nil, color = nil, covered = false}, 70 | {position = {x = 0, z = -2.8}, guid = nil, id = nil, value = nil, color = nil, covered = false}, 71 | {position = {x = 5.7, z = -2.8}, guid = nil, id = nil, value = nil, color = nil, covered = false}, 72 | {position = {x = 11.4, z = -2.8}, guid = nil, id = nil, value = nil, color = nil, covered = false} 73 | } 74 | 75 | agents = { 76 | -- Assassain 77 | ['b97df2'] = {position = {x = 0.04, y = 2, z = -17.84 }, color = "Black", covering = nil, enabled = true}, 78 | 79 | -- Blue agents 80 | ['1e9282'] = {position = {x = 0.04, y = 2, z = -14 }, color = "Blue", covering = nil, enabled = false}, 81 | ['7fdaee'] = {position = {x = 12.34, y = 2, z = -13.97 }, color = "Blue", covering = nil, enabled = true}, 82 | ['99832c'] = {position = {x = 18.07, y = 2, z = -13.97 }, color = "Blue", covering = nil, enabled = true}, 83 | ['d9324a'] = {position = {x = 23.77, y = 2, z = -13.97 }, color = "Blue", covering = nil, enabled = true}, 84 | ['19b2d5'] = {position = {x = 29.47, y = 2, z = -13.97 }, color = "Blue", covering = nil, enabled = true}, 85 | ['0f0ec0'] = {position = {x = 12.34, y = 2, z = -17.8 }, color = "Blue", covering = nil, enabled = true}, 86 | ['d9054c'] = {position = {x = 18.07, y = 2, z = -17.8 }, color = "Blue", covering = nil, enabled = true}, 87 | ['4de840'] = {position = {x = 23.77, y = 2, z = -17.8 }, color = "Blue", covering = nil, enabled = true}, 88 | ['05c73d'] = {position = {x = 29.47, y = 2, z = -17.8 }, color = "Blue", covering = nil, enabled = true}, 89 | 90 | -- Red agents 91 | ['3ef1ca'] = {position = {x = 0.04, y = 2, z = -14 }, color = "Red", covering = nil, enabled = false}, 92 | ['746660'] = {position = {x = -12.22, y = 2, z = -13.97 }, color = "Red", covering = nil, enabled = true}, 93 | ['9cbe84'] = {position = {x = -17.92, y = 2, z = -13.97 }, color = "Red", covering = nil, enabled = true}, 94 | ['6bb4d8'] = {position = {x = -23.62, y = 2, z = -13.97 }, color = "Red", covering = nil, enabled = true}, 95 | ['b48ed4'] = {position = {x = -29.32, y = 2, z = -13.97 }, color = "Red", covering = nil, enabled = true}, 96 | ['5c1be6'] = {position = {x = -12.22, y = 2, z = -17.8 }, color = "Red", covering = nil, enabled = true}, 97 | ['b89ba7'] = {position = {x = -17.92, y = 2, z = -17.8 }, color = "Red", covering = nil, enabled = true}, 98 | ['e1754e'] = {position = {x = -23.62, y = 2, z = -17.8 }, color = "Red", covering = nil, enabled = true}, 99 | ['4a2969'] = {position = {x = -29.32, y = 2, z = -17.8 }, color = "Red", covering = nil, enabled = true}, 100 | 101 | -- Civialians 102 | ['1f53f2'] = {position = {x = -5.7, y = 2, z = -14 }, color = "White", covering = nil, enabled = true}, 103 | ['3d7b86'] = {position = {x = 5.74, y = 2, z = -14 }, color = "White", covering = nil, enabled = true}, 104 | ['e44594'] = {position = {x = -5.7, y = 2, z = -17.84 }, color = "White", covering = nil, enabled = true}, 105 | ['1ebedd'] = {position = {x = 5.74, y = 2, z = -17.84 }, color = "White", covering = nil, enabled = true}, 106 | ['f6786b'] = {position = {x = -5.7, y = 2, z = -21.7 }, color = "White", covering = nil, enabled = true}, 107 | ['f8f6a1'] = {position = {x = 0.04, y = 2, z = -21.7 }, color = "White", covering = nil, enabled = true}, 108 | ['a05e4e'] = {position = {x = 5.74, y = 2, z = -21.7 }, color = "White", covering = nil, enabled = true} 109 | } 110 | 111 | votes = { 112 | [0] = { 113 | ["Orange"] = 0, 114 | ["Yellow"] = 0, 115 | ["Pink"] = 0, 116 | ["Brown"] = 0 117 | }, 118 | [1] = { 119 | ["Teal"] = 0, 120 | ["Purple"] = 0, 121 | ["Green"] = 0, 122 | ["White"] = 0 123 | } 124 | } 125 | 126 | ----------[ Game state ]---------- 127 | gameState = 128 | { 129 | -- Tracks whether the current game has started (-1 = not started, 0 = starting, 1 = started) 130 | status = -1, 131 | 132 | -- Current game is on the first turn or not 133 | firstTurn = true, 134 | 135 | -- Tracks whose turn it currently is 136 | turnTracker = 1, 137 | 138 | -- Tracks how many guesses the team has left 139 | guessesLeft = -1, 140 | 141 | -- Tracks if the current team is able to vote 142 | canVote = false, 143 | 144 | -- Red clue tracker 145 | redClues = {}, 146 | 147 | -- Blue clue tracker 148 | blueClues = {} 149 | } 150 | 151 | ----------[ Game settings ]---------- 152 | settings = 153 | { 154 | -- Shuffle players at start 155 | playerShuffle = true, 156 | 157 | -- Enable codemaster queue 158 | codemasterQueue = true, 159 | 160 | -- Quick color switcher buttons 161 | colorSwitcher = true, 162 | 163 | -- Disable/Enable "inf meta" 164 | cardTilting = false, 165 | 166 | -- Show shooting star background 167 | starBackground = true, 168 | 169 | -- Show table lighting 170 | tableLighting = true, 171 | 172 | -- AFK detection 173 | afkDetection = 174 | { 175 | enabled = false, 176 | threshold = 10 177 | }, 178 | 179 | -- Timer enabled 180 | timer = 181 | { 182 | enabled = true, 183 | initial = 8, 184 | subsequent = 4 185 | }, 186 | 187 | -- Automod functions 188 | automod = 189 | { 190 | autokick = true 191 | } 192 | } 193 | 194 | ----------[ Decks ]---------- 195 | deck = 196 | { 197 | -- The currently selected deck(s) 198 | selected = {}, 199 | 200 | -- Whether or not multiple deck mode is enabled 201 | multiple = false, 202 | 203 | -- The current words for the selected deck 204 | words = nil, 205 | 206 | -- Total amount of decks available for query 207 | totalDecks = 0, 208 | 209 | -- Search term 210 | searchTerm = nil, 211 | 212 | -- Wait id of search delay 213 | searchDelay = nil, 214 | 215 | -- Current page size 216 | pageSize = 10, 217 | 218 | -- Current result page 219 | pageNum = 0, 220 | 221 | -- Tracks deck fetch requests 222 | fetchingDecks = false 223 | } 224 | 225 | ----------[ Game analytics ]---------- 226 | analytics = 227 | { 228 | url = "https://api.tts-codenames.com", 229 | gameID = nil, 230 | host = nil, 231 | clueID = nil, 232 | sessions = {} 233 | } 234 | 235 | function onload(saveState) 236 | 237 | -- Codenames script version 238 | version = "3.2.3" 239 | 240 | ----------[ Script object initialization ]---------- 241 | -- Control panels and table objects 242 | customTable = getObjectFromGUID("bab013") 243 | tableObject = getObjectFromGUID("0a61c6") 244 | 245 | -- Game objects 246 | cardTemplates = getObjectFromGUID("2fb333") 247 | extraBlue = getObjectFromGUID("1e9282") 248 | extraRed = getObjectFromGUID("3ef1ca") 249 | redToken = getObjectFromGUID("f3cdf2") 250 | blueToken = getObjectFromGUID("3f75b3") 251 | buttonRed = getObjectFromGUID("f16a9a") 252 | buttonBlue = getObjectFromGUID("c91f34") 253 | 254 | ----------[ Interactable objects ]---------- 255 | -- Control panels and table objects 256 | customTable.interactable = false 257 | tableObject.interactable = false 258 | 259 | -- Game objects 260 | cardTemplates.interactable = false 261 | extraBlue.interactable = false 262 | extraRed.interactable = false 263 | redToken.interactable = false 264 | blueToken.interactable = false 265 | 266 | -- Get the list of decks 267 | api_getDecks() 268 | 269 | buttonBlue.createButton({ 270 | label="[END TURN]", click_function="endTurn", function_owner=self, 271 | position={0,-0.15,0}, rotation={0,90,0}, height=1000, width=2000, font_size=10 272 | }) 273 | 274 | buttonRed.createButton({ 275 | label="[END TURN]", click_function="endTurn", function_owner=self, 276 | position={0,-0.15,0}, rotation={0,90,0}, height=1000, width=2000, font_size=10 277 | }) 278 | 279 | -- Make agents non-selectable 280 | for agentGUID, agentData in pairs(agents) do 281 | local agentObject = getObjectFromGUID(agentGUID) 282 | if agentObject then 283 | agentObject.drag_selectable = false 284 | end 285 | end 286 | 287 | -- Change seated players views 288 | seatedPlayers = Player.getPlayers() 289 | for _, player in ipairs(seatedPlayers) do 290 | player.print(chatDisclaimer, {1, 1, 1}) 291 | Wait.frames(function() 292 | player.lookAt({ 293 | position = {0, 0, 0}, 294 | pitch = 60, 295 | yaw = 0, 296 | distance = 45 297 | }) 298 | end, 1) 299 | end 300 | 301 | -- Load save state - if one exists 302 | if saveState != "" then 303 | local decodedSaveState = JSON.decode(saveState) 304 | 305 | -- TODO: 306 | -- votes 307 | -- currently selected decks 308 | -- table ui elements 309 | 310 | if decodedSaveState.version == version and decodedSaveState.gameState.status == 1 then 311 | -- Load the previous gameState 312 | gameState = decodedSaveState.gameState 313 | 314 | -- Load the previous settings and update the view 315 | settings = decodedSaveState.settings 316 | tableObject.call("updateSettings") 317 | 318 | -- Load the analytics data 319 | analytics = decodedSaveState.analytics 320 | 321 | -- Load the card data and update their UI 322 | cards = decodedSaveState.cards 323 | dealCards() 324 | 325 | -- Update the enabled property for agents 326 | agents = decodedSaveState.agents 327 | for agentGUID, agentData in pairs(decodedSaveState.agents) do 328 | if agents[agentGUID] ~= nil and agents[agentGUID].covering ~= nil then 329 | coverCard(agents[agentGUID].covering, agentGUID) 330 | end 331 | end 332 | end 333 | end 334 | 335 | for _, player in ipairs(Player.getPlayers()) do 336 | if player.host then 337 | analytics.host = player.steam_id 338 | break 339 | end 340 | end 341 | 342 | -- Start the AFK loop checker 343 | if settings.afkDetection.enabled then 344 | Timer.create({ 345 | identifier = "afkLoop", 346 | function_name = "afkCheckLoop", 347 | function_owner = self, 348 | delay = 1, 349 | repetitions = 0 350 | }) 351 | end 352 | end 353 | 354 | function onSave() 355 | local saveData = {} 356 | 357 | saveData.version = version 358 | saveData.gameState = gameState 359 | saveData.settings = settings 360 | saveData.analytics = analytics 361 | saveData.cards = cards 362 | saveData.agents = agents 363 | 364 | return JSON.encode(saveData) 365 | end 366 | 367 | ------------------------------------------------------------------------ 368 | --------------------[ SETTERS/GETTERS FOR SETTINGS ]-------------------- 369 | ------------------------------------------------------------------------ 370 | 371 | function setShuffle(enabled) settings.playerShuffle = enabled end 372 | function getShuffle() return settings.playerShuffle end 373 | 374 | function setQueue(enabled) settings.codemasterQueue = enabled end 375 | function getQueue() return settings.codemasterQueue end 376 | 377 | function setSwitcher(enabled) settings.colorSwitcher = enabled end 378 | function getSwitcher() return settings.colorSwitcher end 379 | 380 | function setTilting(enabled) 381 | settings.cardTilting = enabled 382 | end 383 | function getTilting() return settings.cardTilting end 384 | 385 | function setAfk(enabled) 386 | settings.afkDetection.enabled = enabled 387 | Timer.destroy("afkLoop") 388 | if settings.afkDetection.enabled then 389 | Timer.create({ 390 | identifier = "afkLoop", 391 | function_name = "afkCheckLoop", 392 | function_owner = self, 393 | delay = 1, 394 | repetitions = 0 395 | }) 396 | end 397 | end 398 | 399 | function setTimer(enabled) 400 | settings.timer.enabled = enabled 401 | if settings.timer.enabled then 402 | startTime() 403 | else 404 | -- Disable the timer 405 | end 406 | end 407 | function getTimer() return settings.timer.enabled end 408 | 409 | function setTimer1Time(value) settings.timer.initial = value end 410 | function getTimer1Time() return settings.timer.initial end 411 | 412 | function setTimer2Time(value) settings.timer.subsequent = value end 413 | function getTimer2Time() return settings.timer.subsequent end 414 | 415 | function setAutokick(enabled) settings.automod.autokick = enabled end 416 | function getAutokick() return settings.automod.autokick end 417 | 418 | function toggleDeckMode() deck.multiple = not deck.multiple end 419 | function getMultipleDeckMode() return deck.multiple end 420 | 421 | function afkCheckLoop() 422 | -- Temporarily removed 423 | --[[if settings.afkDetection.enabled then 424 | local colorsToCheck = { 425 | "Teal", 426 | "Orange", 427 | "Purple", 428 | "Yellow", 429 | "Green", 430 | "Pink", 431 | "White", 432 | "Brown" 433 | } 434 | for _, color in ipairs(colorsToCheck) do 435 | local player = Player[color] 436 | if player.seated then 437 | -- Only if there's a player in this seat 438 | local playerInfo = afkTracker[color] 439 | local pos = player.getPointerPosition() 440 | if playerInfo == nil then 441 | -- No player information exists for this color yet 442 | afkTracker[color] = { 443 | timestamp = os.time(), 444 | cursorPosition = pos 445 | } 446 | elseif round(playerInfo.cursorPosition.x, 2) != round(pos.x, 2) or round(playerInfo.cursorPosition.y, 2) != round(pos.y, 2) or round(playerInfo.cursorPosition.z, 2) != round(pos.z, 2) then 447 | -- Cursor position isn't the same. log the new position 448 | playerInfo.timestamp = os.time() 449 | playerInfo.cursorPosition = pos 450 | afkTracker[color] = playerInfo 451 | else 452 | local timeToAFK = round((settings.afkDetection.threshold * 60) - (os.time() - playerInfo.timestamp), 0) 453 | if timeToAFK <= 0 then 454 | -- Switch player to spectator 455 | player.broadcast("You have been moved to spectator to AFKing for more than " .. tostring(settings.afkDetection.threshold) .. " minute(s).", redColor) 456 | printToAll("Player " .. player.steam_name .. " has been moved to spectator for being AFK for more than " .. tostring(settings.afkDetection.threshold) .. " minute(s).", stringColorToRGB(color)) 457 | player.changeColor("Grey") 458 | elseif timeToAFK <= 5 then 459 | player.broadcast("You will be removed due to AFK in: " .. timeToAFK, redColor) 460 | end 461 | end 462 | end 463 | end 464 | end]] 465 | end 466 | 467 | function previousPage() 468 | if (deck.pageNum - 1 < 0) or deck.fetchingDecks then 469 | return 470 | end 471 | 472 | deck.pageNum = deck.pageNum - 1 473 | api_getDecks() 474 | end 475 | 476 | function nextPage() 477 | if (deck.pageNum + 1 > math.ceil(deck.totalDecks / deck.pageSize) - 1) or deck.fetchingDecks then 478 | return 479 | end 480 | 481 | deck.pageNum = deck.pageNum + 1 482 | api_getDecks() 483 | end 484 | 485 | function searchDecks(searchTerm) 486 | deck.searchTerm = searchTerm 487 | 488 | if deck.searchDelay then 489 | Wait.stop(deck.searchDelay) 490 | deck.searchDelay = nil 491 | end 492 | 493 | deck.searchDelay = Wait.time(function() 494 | deck.pageNum = 0 495 | api_getDecks() 496 | deck.searchDelay = nil 497 | end, 0.5) 498 | end 499 | 500 | function clueEntered(player, value) 501 | if value:match("\n") then 502 | local color = player.color 503 | 504 | -- Reset the text box 505 | local resetInput = { 506 | text = "", 507 | placeholder = "Enter clue here" 508 | } 509 | UI.setAttributes(color:lower() .. "ClueText", resetInput) 510 | 511 | -- if the game hasn't been started, a clue cannot be entered 512 | if gameState.status ~= 1 then 513 | Player[color].broadcast("[a020f0]» [da1918]ERROR: [ffffff]You must start a game to enter a clue! [a020f0]«") 514 | return 515 | end 516 | 517 | -- Make sure that it's the current codemaster's turn 518 | if (color == "Blue" and gameState.turnTracker == 0) or (color == "Red" and gameState.turnTracker == 1) then 519 | Player[color].broadcast("[a020f0]» [da1918]ERROR: [ffffff]It's not your turn to enter a clue! [a020f0]«") 520 | return 521 | end 522 | 523 | -- Remove the newline, trim the clue, and convert to lowercase 524 | value = value:gsub("\n", ""):match("%s*(.-)%s*$"):lower() 525 | 526 | -- Parse the entered clue into its respective parts 527 | local clue, number, error = getClueDetails(value) 528 | 529 | -- Handle clue parsing errors 530 | if error then 531 | -- Clue wasn't able to be parsed - general error 532 | Player[color].broadcast("[a020f0]» [da1918]ERROR: [ffffff]Invalid clue. Please enter a valid clue and push ENTER! [a020f0]«") 533 | return 534 | elseif number != "inf" and tonumber(number) > 9 then 535 | -- Clue was larger than 9 words (not 0 or inf) 536 | Player[color].broadcast("[a020f0]» [da1918]ERROR: [ffffff]Clues cannot be for more than 9 words. [a020f0]«") 537 | return 538 | elseif string.len(clue) > 20 then 539 | -- Clue was larger than 20 characters 540 | Player[color].broadcast("[a020f0]» [da1918]ERROR: [ffffff]Clues cannot be longer than 20 characters. [a020f0]«") 541 | return 542 | end 543 | 544 | -- Don't allow a clue that isn't covered 545 | for cardIndex, cardData in ipairs(cards) do 546 | if not cardData.covered and cardData.value:lower() == clue:lower() then 547 | Player[color].broadcast("[a020f0]» [da1918]ERROR: [ffffff]Invalid clue. That word has not been covered yet, so it may not be used! [a020f0]«") 548 | return 549 | end 550 | end 551 | 552 | -- Track remaining clues 553 | if number == "inf" then 554 | gameState.guessesLeft = -1 555 | elseif number == "0" then 556 | gameState.guessesLeft = -1 557 | else 558 | gameState.guessesLeft = tostring(number) + 1 559 | end 560 | 561 | -- Encode the finished clue 562 | encodeClue(color, clue .. " - " .. number:gsub("inf", "∞")) 563 | 564 | -- Enable voting for the current team 565 | gameState.canVote = true 566 | 567 | -- Send analytics data for the a new clue 568 | api_newClue(clue, (number == "inf" and -1 or number), Player[color].steam_id) 569 | end 570 | end 571 | 572 | function getClueDetails(processedClue) 573 | -- How many hyphens are there? 574 | local clue, number 575 | local _, hyphenCount = string.gsub(processedClue, "%-", "") 576 | local _, spaceCount = string.gsub(processedClue, "%s", "") 577 | 578 | if hyphenCount == 0 then 579 | -- Single word with space (or no space) as delimiter 580 | if spaceCount > 1 then 581 | return nil, nil, true 582 | end 583 | 584 | local checks = { 585 | "^(%a+)(%s*)(%d+)$", 586 | "^(%a+)(%s+)(inf)$" 587 | } 588 | 589 | for _, check in ipairs(checks) do 590 | local status, clue, _, number = pcall(function() return string.match(processedClue, check) end) 591 | if status then 592 | -- Parsing successful - check for nil values just in case 593 | if clue != nil and number != nil then 594 | -- Return the clue and number 595 | return clue, number, false 596 | end 597 | end 598 | end 599 | 600 | -- No valid clues detected 601 | return nil, nil, true 602 | 603 | elseif hyphenCount == 1 then 604 | -- Either a hypenated word with a space (or no space) as delimiter 605 | -- or a single word with a hyphen (and possibly spaces) as delimiter 606 | if spaceCount > 2 then 607 | return nil, nil, true 608 | end 609 | 610 | local checks = { 611 | "^(%a+%-%a+)(%s*)(%d+)$", 612 | "^(%a+%-%a+)(%s+)(inf)$", 613 | "^(%a+)(%s*%-%s*)(%d+)$", 614 | "^(%a+)(%s*%-%s*)(inf)$" 615 | } 616 | 617 | for _, check in ipairs(checks) do 618 | local status, clue, _, number = pcall(function() return string.match(processedClue, check) end) 619 | if status then 620 | -- Parsing successful - check for nil values just in case 621 | if clue != nil and number != nil then 622 | -- Return the clue and number 623 | return clue, number, false 624 | end 625 | end 626 | end 627 | 628 | -- No valid clues detected 629 | return nil, nil, true 630 | 631 | elseif hyphenCount == 2 then 632 | 633 | if spaceCount > 2 then 634 | return nil, nil, true 635 | end 636 | 637 | local checks = { 638 | "^(%a+%-%a+)(%s*%-%s*)(%d+)$", 639 | "^(%a+%-%a+)(%s*%-%s*)(inf)$" 640 | } 641 | 642 | for _, check in ipairs(checks) do 643 | local status, clue, _, number = pcall(function() return string.match(processedClue, check) end) 644 | if status then 645 | -- Parsing successful - check for nil values just in case 646 | if clue != nil and number != nil then 647 | -- Return the clue and number 648 | return clue, number, false 649 | end 650 | end 651 | end 652 | 653 | -- No valid clues detected 654 | return nil, nil, true 655 | 656 | else 657 | -- Clue has too many hyphens 658 | return nil, nil, true 659 | end 660 | end 661 | 662 | function rotateclues() 663 | for cardIndex, cardData in ipairs(cards) do 664 | local card = getObjectFromGUID(cardData.guid) 665 | if card.interactable then 666 | card.setRotation({0, 180, 0}) 667 | end 668 | end 669 | end 670 | 671 | function encodeClue(color, clue) 672 | local finishedClue 673 | local cluePosition 674 | local token = color == "Red" and redToken or blueToken 675 | local broadcastTo = color == "Red" and {"Orange", "Yellow", "Pink", "Brown"} or {"Teal", "Purple", "Green", "White"} 676 | 677 | local clues = color == "Red" and gameState.redClues or gameState.blueClues 678 | local xPos = color == "Red" and -21.05 or 21.05 679 | local deletedClues = false 680 | local validClues = {} 681 | 682 | -- Check for deleted clues and shift the remaining if need be 683 | if #clues > 0 then 684 | -- Check for deleted clues 685 | for _,clue in ipairs(color == "Red" and gameState.redClues or gameState.blueClues) do 686 | if getObjectFromGUID(clue) ~= nil then 687 | table.insert(validClues, clue) 688 | else 689 | deletedClues = true 690 | end 691 | end 692 | end 693 | 694 | if deletedClues then 695 | if color == "Red" then 696 | gameState.redClues = validClues 697 | else 698 | gameState.blueClues = validClues 699 | end 700 | for i, clue in ipairs(validClues) do 701 | local clueObject = getObjectFromGUID(clue) 702 | clueObject.setPosition({xPos, 1.5, (12 - (((i - 1) % 8) * 2.05))}) 703 | clueObject.setRotation({0, 180, 0}) 704 | end 705 | end 706 | local finishedClue = token.clone({ 707 | position = token.getPosition(), 708 | snap_to_grid = true, 709 | callback_function = function(clue) table.insert(color == "Red" and gameState.redClues or gameState.blueClues, clue.guid) end 710 | }) 711 | finishedClue.createButton({ 712 | label=clue, function_owner=finishedClue, click_function="clue", 713 | position={0,0.2,0}, height=0, width=0, font_size=500, rotation={0,0,0}, scale={2, 2, 4/3}, font_color={1,1,1} 714 | }) 715 | finishedClue.setPosition({xPos, 1.5, (12 - ((#(color == "Red" and gameState.redClues or gameState.blueClues) % 8) * 2.05))}) 716 | finishedClue.setRotation({0, 180, 0}) 717 | finishedClue.setLock(false) 718 | finishedClue.drag_selectable = false 719 | finishedClue.setLuaScript( 720 | "function onload()\n" 721 | .. "self.createButton({\n" 722 | .. "label='" .. clue .. "', click_function='nullFunction', function_owner=self,\n" 723 | .. "position={0,0.2,0}, height=0, width=0, font_size=500, rotation={0,0,0}, scale={2, 2, 4/3}, font_color={1,1,1}\n" 724 | .. "})\n" 725 | .. "end\n" 726 | ) 727 | printToAll("[a020f0]» " .. (color == "Red" and "[da1918]RED" or "[1f87ff]BLUE") .. " [ffffff]team's clue is: " .. (color == "Red" and "[da1918]" or "[1f87ff]") .. clue .. " [a020f0]«") 728 | for _, playerColor in ipairs(broadcastTo) do 729 | Player[playerColor].broadcast("[a020f0]» [ffffff]Your clue is: " .. (color == "Red" and "[da1918]" or "[1f87ff]") .. clue .. " [a020f0]«") 730 | end 731 | end 732 | 733 | function onObjectEnterContainer(deck, card) 734 | -- First to check that we only do this on the first grouping 735 | if #deck.getObjects() == 2 then 736 | deck.destruct() 737 | Wait.frames(function() dealCards() end, 180) 738 | end 739 | end 740 | 741 | function onPlayerConnect(player) 742 | -- Send analytics disclaimer 743 | player.print(chatDisclaimer, {1, 1, 1}) 744 | 745 | -- Check the blacklist for the player 746 | WebRequest.get(analytics.url .. "/games/player/blacklisted/" .. player.steam_id, function(responseRaw) 747 | if responseRaw.is_done then 748 | local response = JSON.decode(responseRaw.text) 749 | 750 | if response.record and response.record.dateAdded ~= nil then 751 | if settings.automod.autokick then 752 | player.kick() 753 | end 754 | printToAll("\n", {1, 1, 1}) 755 | printToAll(player.steam_name .. " was found in the global Codenames blacklist!", redColor) 756 | printToAll("SteamID : " .. player.steam_id, redColor) 757 | printToAll("Date Added : " .. response.record.dateAdded, redColor) 758 | printToAll("Reason : " .. response.record.reason, redColor) 759 | printToAll("\n", {1, 1, 1}) 760 | end 761 | end 762 | end) 763 | end 764 | 765 | function onPlayerDisconnect(player) 766 | -- Destroy the timer for the disclaimer, if one exists 767 | Timer.destroy(player.steam_id) 768 | 769 | -- End session with player if one exists 770 | if gameState.status == 1 then 771 | if analytics.sessions[player.steam_id] ~= nil then 772 | api_playerSessionEnd(analytics.sessions[player.steam_id]) 773 | end 774 | end 775 | end 776 | 777 | function onPlayerChangeColor(color) 778 | -- Change the player's view 779 | if color != "Grey" then 780 | Player[color].lookAt({ 781 | position = {0, 0, 0}, 782 | pitch = 60, 783 | yaw = 0, 784 | distance = 45 785 | }) 786 | end 787 | 788 | -- Reset the player's vote (if necessary) 789 | if gameState.status == 1 then 790 | for playerColor, voteData in pairs(votes[gameState.turnTracker]) do 791 | if not Player[playerColor].seated and voteData ~= 0 then 792 | local card = voteData 793 | voteData = 0 794 | updateVoteUI(card) 795 | end 796 | end 797 | end 798 | 799 | local endSession = {} 800 | 801 | -- Resolve team changes, if necessary 802 | if color == "Red" or color == "Blue" then 803 | Player[color].team = "Hearts" 804 | elseif color == "Grey" then 805 | local spectators = Player.getSpectators() 806 | for _,spec in ipairs(spectators) do 807 | if analytics.sessions[spec.steam_id] ~= nil then 808 | table.insert(endSession, analytics.sessions[spec.steam_id]) 809 | end 810 | if spec.team != "None" then 811 | spec.team = "None" 812 | end 813 | end 814 | else 815 | if Player[color].team != "None" then 816 | Player[color].team = "None" 817 | end 818 | end 819 | 820 | if color ~= "Grey" and color ~= "Black" then 821 | if gameState.status == 1 and analytics.sessions[Player[color].steam_id] == nil then 822 | api_playerSessionStart(Player[color].steam_id) 823 | end 824 | elseif gameState.status == 1 and (color == "Grey" or color == "Black") and #endSession ~= 0 then 825 | for _, sessionID in ipairs(endSession) do 826 | api_playerSessionEnd(sessionID) 827 | end 828 | end 829 | end 830 | 831 | function startTime() 832 | if settings.timer.enabled then 833 | if gameState.firstTurn then 834 | tableObject.call("startTimer", settings.timer.initial * 60) 835 | else 836 | tableObject.call("startTimer", settings.timer.subsequent * 60) 837 | end 838 | end 839 | end 840 | 841 | -- Deck fetching 842 | function setDeck(deckID) 843 | if deckID == nil then 844 | deck.selected = {} 845 | return 846 | end 847 | 848 | if not deck.multiple then 849 | deck.selected = { deckID } 850 | else 851 | local exists = 0 852 | for deckIndex, deckGUID in ipairs(deck.selected) do 853 | if deckGUID == deckID then 854 | exists = deckIndex 855 | end 856 | end 857 | 858 | if exists == 0 then 859 | -- Insert the deck into the selected list 860 | table.insert(deck.selected, deckID) 861 | else 862 | -- Remove the deck from the list 863 | table.remove(deck.selected, exists) 864 | end 865 | end 866 | end 867 | 868 | -- Restrict newGame permissions to Red/Blue/Promoted/Host 869 | function startGame(player) 870 | -- Check to see whether a new game is currently being set up 871 | if gameState.status == 0 then 872 | player.broadcast("[a020f0]» [da1918]ERROR: [ffffff]A new game is already starting. Please wait. [a020f0]«") 873 | return 874 | end 875 | 876 | -- Check to see if a deck has been chosen 877 | if #deck.selected == 0 then 878 | player.broadcast("[a020f0]» [da1918]ERROR: [ffffff]You must select a deck before starting. [a020f0]«") 879 | return 880 | end 881 | 882 | -- A new game is currently starting - block any other new game requests 883 | gameState.status = 0 884 | 885 | -- Reset the game to its original state 886 | resetGame() 887 | 888 | if settings.playerShuffle then 889 | startLuaCoroutine(tableObject, "shufflePlayers") 890 | elseif settings.codemasterQueue then 891 | startLuaCoroutine(tableObject, "swapCodemasters") 892 | else 893 | api_gameStart() 894 | end 895 | end 896 | 897 | function resetGame() 898 | -- Reset card data 899 | for cardNum, cardData in ipairs(cards) do 900 | local cardObject = getObjectFromGUID(cardData.guid) 901 | if cardObject then 902 | cardObject.interactable = true 903 | cardObject.setLock(false) 904 | end 905 | 906 | cardData.id = nil 907 | cardData.value = nil 908 | cardData.color = nil 909 | cardData.covered = false 910 | end 911 | 912 | -- Reset the agent cards to their start positions 913 | for guid, agentData in pairs(agents) do 914 | local agentObject = getObjectFromGUID(guid) 915 | if agentObject then 916 | local isExtraCard = (agentObject.guid == extraBlue.guid) or (agentObject.guid == extraRed.guid) 917 | agentObject.interactable = not isExtraCard 918 | agentObject.setLock(isExtraCard) 919 | agentObject.setPositionSmooth(isExtraCard and {0, -2, -14} or agentData.position) 920 | agentObject.setRotationSmooth({0, 180, 180}) 921 | agents[agentObject.guid].enabled = not isExtraCard 922 | agents[agentObject.guid].covering = nil 923 | end 924 | end 925 | 926 | -- Delete clue tiles 927 | for _, clueList in ipairs({gameState.redClues, gameState.blueClues}) do 928 | for _, clue in ipairs(clueList) do 929 | local clueTile = getObjectFromGUID(clue) 930 | if clueTile then 931 | clueTile.destruct() 932 | end 933 | end 934 | end 935 | 936 | -- Reset team votes 937 | for turnNum, team in pairs(votes) do 938 | for seatColor, vote in pairs(team) do 939 | votes[turnNum][seatColor] = 0 940 | end 941 | end 942 | 943 | -- Reset game state variables 944 | gameState.firstTurn = true 945 | gameState.guessesLeft = -1 946 | gameState.canVote = false 947 | gameState.redClues = {} 948 | gameState.blueClues = {} 949 | end 950 | 951 | function setupGame() 952 | -- Determine which team should go first (0 = RED; 1 = BLUE) 953 | gameState.turnTracker = math.random(2) - 1 954 | 955 | -- Shuffle the keymap 956 | local bank = {"Red", "Red", "Red", "Red", "Red", "Red", "Red", "Red", "Blue", "Blue", "Blue", "Blue", "Blue", "Blue", "Blue", "Blue", "White", "White", "White", "White", "White", "White", "White", "Black"} 957 | if gameState.turnTracker == 0 then 958 | -- Add another red 959 | bank = table.insert(bank, "Red") 960 | else 961 | -- Add another blue 962 | bank = table.insert(bank, "Blue") 963 | end 964 | 965 | local words = deck.words 966 | 967 | for i = 1, 25, 1 do 968 | local nextCard = table.remove(words, math.random(1, #words)) 969 | cards[i].id = nextCard.id 970 | cards[i].value = nextCard.word:upper() 971 | cards[i].color = table.remove(bank, math.random(1, #bank)) 972 | end 973 | 974 | -- Choose the correct double agent card for which team goes first 975 | local extraCard = gameState.turnTracker == 0 and extraRed or extraBlue 976 | 977 | -- Set the correct double card to red 978 | extraCard.setLock(false) 979 | extraCard.setPositionSmooth(agents[extraCard.guid].position) 980 | extraCard.interactable = true 981 | agents[extraCard.guid].enabled = true 982 | 983 | -- Change the turn indicator 984 | local previousTurn = (gameState.turnTracker == 0 and "blue" or "red") .. "Turn" 985 | local currentTurn = (gameState.turnTracker == 0 and "red" or "blue") .. "Turn" 986 | local teamColorTurn = gameState.turnTracker == 0 and "Red" or "Blue" 987 | tableObject.UI.setAttribute(previousTurn, "active", false) 988 | tableObject.UI.setAttribute(currentTurn, "active", true) 989 | tableObject.UI.setAttribute(previousTurn .. "BG", "active", false) 990 | tableObject.UI.setAttribute(currentTurn .. "BG", "active", true) 991 | tableObject.UI.setAttribute("timer", "outline", teamColorTurn) 992 | printToAll("[a020f0]» " .. (teamColorTurn == "Red" and "[da1918]RED" or "[1f87ff]BLUE") .. " [ffffff]team's turn! [a020f0]«") 993 | 994 | dealCards() 995 | 996 | -- Start the timer 997 | gameState.firstTurn = true 998 | end 999 | 1000 | -- Deals the cards on the board 1001 | function dealCards() 1002 | for i = 1, 25, 1 do 1003 | local cardObject = getObjectFromGUID(cards[i].guid) 1004 | if cards[i].guid == nil or cardObject == nil then 1005 | -- Get new card if it doesn't exist 1006 | local c = cardTemplates.takeObject({ 1007 | position = {cards[i].position.x, 1.03, cards[i].position.z}, 1008 | rotation = {0, 180, 0}, 1009 | snap_to_grid = true, 1010 | callback_function = function(card) setCardData(card, i) end 1011 | }) 1012 | else 1013 | -- Set the UI 1014 | setCardData(cardObject, i) 1015 | end 1016 | end 1017 | end 1018 | 1019 | function setCardData(card, i) 1020 | cards[i].guid = card.guid 1021 | 1022 | card.call("setData", { 1023 | position = cards[i].position, 1024 | text = cards[i].value, 1025 | color = cards[i].color 1026 | }) 1027 | 1028 | card.setLock(false) 1029 | card.drag_selectable = false 1030 | 1031 | card.setName(cards[i].value) 1032 | 1033 | -- Once the last card has loaded in, allow a new game to be started 1034 | if i == 25 and gameState.status == 0 then 1035 | startTime() 1036 | gameState.status = 1 1037 | end 1038 | end 1039 | 1040 | function findClosestCard(threshold, position) 1041 | local closestCard = 1042 | { 1043 | index = nil, 1044 | distance = nil 1045 | } 1046 | 1047 | for cardIndex,_ in ipairs(cards) do 1048 | local dX = position.x - cards[cardIndex].position.x 1049 | local dZ = position.z - cards[cardIndex].position.z 1050 | 1051 | local d = math.sqrt((dX^2) + (dZ^2)) 1052 | 1053 | if d < threshold then 1054 | if closestCard.index == nil or d < closestCard.distance then 1055 | closestCard.index = cardIndex 1056 | closestCard.distance = d 1057 | end 1058 | end 1059 | end 1060 | 1061 | return closestCard.index 1062 | end 1063 | 1064 | function onObjectDrop(color, agent) 1065 | -- Ensure that the dropped object is an agent card 1066 | if gameState.status ~= 1 or agent.tag ~= "Tile" or agents[agent.guid] == nil then 1067 | return 1068 | end 1069 | 1070 | -- Find the closest card position to the dropped agent 1071 | local cardIndex = findClosestCard(1.65, agent.getPosition()) 1072 | if cardIndex == nil or cards[cardIndex].covered then 1073 | -- Either no close card found, or card is already covered 1074 | agent.setPositionSmooth(agents[agent.guid].position) 1075 | return 1076 | end 1077 | 1078 | -- Check to see if the agent and the card are the same color 1079 | local agentColor = agents[agent.guid].color 1080 | local cardColor = cards[cardIndex].color 1081 | if cardColor ~= agentColor then 1082 | agent.setPositionSmooth(agents[agent.guid].position) 1083 | Player[color].broadcast("[a020f0]» [da1918]ERROR: [ffffff]An agent has been placed incorrectly. You placed a " .. agentColor .. " agent on a " .. cardColor .. " card. [a020f0]«") 1084 | return 1085 | end 1086 | 1087 | -- Card has been marked correctly 1088 | gameState.guessesLeft = gameState.guessesLeft - 1 1089 | cards[cardIndex].covered = true 1090 | 1091 | coverCard(cardIndex, agent.guid) 1092 | 1093 | -- Send analytics data for the guess 1094 | local players = "" 1095 | local correct = ((gameState.turnTracker == 0 and cardColor == "Red") or (gameState.turnTracker == 1 and cardColor == "Blue")) and "TRUE" or "FALSE" 1096 | for color,_ in pairs(votes[gameState.turnTracker]) do 1097 | if Player[color].seated then 1098 | players = players .. ((players == "") and Player[color].steam_id or (',' .. Player[color].steam_id)) 1099 | end 1100 | end 1101 | api_clueGuess(players, cards[cardIndex].id, correct, cardColor:upper()) 1102 | 1103 | local messageColor = { 1104 | ["Red"] = "da1918", 1105 | ["Blue"] = "1f87ff", 1106 | ["White"] = "ffffff", 1107 | ["Black"] = "191919" 1108 | } 1109 | 1110 | if correct == "TRUE" then 1111 | -- Play the correct sound effect 1112 | tableObject.AssetBundle.playTriggerEffect(0) 1113 | printToAll("[a020f0]» [31b32b][✓] [" .. messageColor[cardColor] .. "]" .. cardColor:upper() .. " [ffffff]team has correctly guessed: [" .. messageColor[cardColor] .."]" .. cards[cardIndex].value:upper() .. " [a020f0]«") 1114 | else 1115 | -- Play the wrong sound effect 1116 | tableObject.AssetBundle.playTriggerEffect(1) 1117 | local guessingTeam = gameState.turnTracker == 0 and "Red" or "Blue" 1118 | printToAll("[a020f0]» [da1918][✗] [" .. messageColor[guessingTeam] .. "]" .. guessingTeam:upper() .. " [ffffff]team has incorrectly guessed: [" .. messageColor[cardColor] .."]" .. cards[cardIndex].value:upper() .. " [a020f0]«") 1119 | end 1120 | 1121 | -- Check to see if either red or blue won 1122 | local redWon = true 1123 | local blueWon = true 1124 | for i = 1, 25, 1 do 1125 | if cards[i].color == "Red" and not cards[i].covered then 1126 | redWon = false 1127 | elseif cards[i].color == "Blue" and not cards[i].covered then 1128 | blueWon = false 1129 | end 1130 | 1131 | if not redWon and not blueWon then 1132 | -- Skip unnecessary iterations 1133 | break 1134 | end 1135 | end 1136 | 1137 | if agentColor == "Black" or blueWon or redWon then 1138 | -- End game scenario 1139 | 1140 | -- Analytics data for end of game 1141 | if (agentColor == "Black" and gameState.turnTracker == 0) or blueWon then 1142 | -- Red placed black card, blue wins 1143 | -- or blue placed all of their cards 1144 | broadcastToAll("[a020f0]» [1f87ff]BLUE [ffffff]team wins! [a020f0]«") 1145 | api_gameEnd("BLUE") 1146 | elseif (agentColor == "Black" and gameState.turnTracker == 1) or redWon then 1147 | -- Blue placed black card, red wins 1148 | -- or red placed all of their cards 1149 | broadcastToAll("[a020f0]» [da1918]RED [ffffff]team wins! [a020f0]«") 1150 | api_gameEnd("RED") 1151 | end 1152 | 1153 | -- Move the remaining agents to their codes 1154 | endGame() 1155 | elseif gameState.guessesLeft == 0 or agentColor == "White" or (agentColor == "Red" and gameState.turnTracker == 1) or (agentColor == "Blue" and gameState.turnTracker == 0) then 1156 | toggleTurns() 1157 | else 1158 | -- Show the pass turn button and change color 1159 | tableObject.UI.setAttributes("passTurn", { 1160 | color = gameState.turnTracker == 0 and "#da1918" or "#1f87ff", 1161 | active = true 1162 | }) 1163 | end 1164 | end 1165 | 1166 | function playerVote(color, card) 1167 | -- Update the player's vote 1168 | if color == "Grey" then 1169 | -- Ignore votes from spectators 1170 | return 1171 | end 1172 | 1173 | if color == "Red" or color == "Blue" or color == "Black" then 1174 | Player[color].broadcast("[a020f0]» [da1918]ERROR: [ffffff]You cannot vote! [a020f0]«") 1175 | return 1176 | elseif votes[gameState.turnTracker][color] == nil then 1177 | Player[color].broadcast("[a020f0]» [da1918]ERROR: [ffffff]It's not your turn to vote! [a020f0]«") 1178 | return 1179 | elseif not gameState.canVote then 1180 | Player[color].broadcast("[a020f0]» [da1918]ERROR: [ffffff]You can't vote until you've been given a clue! [a020f0]«") 1181 | return 1182 | end 1183 | 1184 | if votes[gameState.turnTracker][color] == 0 then 1185 | votes[gameState.turnTracker][color] = card 1186 | updateVoteUI(card) 1187 | else 1188 | if votes[gameState.turnTracker][color] == card then 1189 | -- Remove this player's vote 1190 | votes[gameState.turnTracker][color] = 0 1191 | updateVoteUI(card) 1192 | else 1193 | -- Switch the player's current vote to this card 1194 | local previous = votes[gameState.turnTracker][color] 1195 | votes[gameState.turnTracker][color] = card 1196 | updateVoteUI(previous) 1197 | updateVoteUI(card) 1198 | end 1199 | end 1200 | 1201 | -- Check to see if a vote has passed 1202 | local votePassed = false 1203 | local voteCard = nil 1204 | for playerColor, voteData in pairs(votes[gameState.turnTracker]) do 1205 | if Player[playerColor].seated and not Player[playerColor].blindfolded then 1206 | if (voteCard == nil and voteData ~= 0) or voteData == voteCard then 1207 | votePassed = true 1208 | voteCard = voteData 1209 | else 1210 | votePassed = false 1211 | break 1212 | end 1213 | end 1214 | end 1215 | 1216 | if votePassed then 1217 | -- Remove all vote indicators and reset votes 1218 | local cardsToClear = {} 1219 | for playerColor, voteData in pairs(votes[gameState.turnTracker]) do 1220 | -- Update the vote UI first 1221 | if voteData ~= 0 then 1222 | table.insert(cardsToClear, voteData) 1223 | votes[gameState.turnTracker][playerColor] = 0 1224 | end 1225 | end 1226 | 1227 | for _, card in ipairs(cardsToClear) do 1228 | updateVoteUI(card) 1229 | end 1230 | 1231 | -- Vote passed to pass turn 1232 | if card == 26 then 1233 | toggleTurns() 1234 | return 1235 | end 1236 | 1237 | -- Card has been marked correctly 1238 | gameState.guessesLeft = gameState.guessesLeft - 1 1239 | cards[card].covered = true 1240 | 1241 | -- Cover the card 1242 | coverCard(card, nil) 1243 | 1244 | -- Send analytics data for the guess 1245 | local players = "" 1246 | local correct = ((gameState.turnTracker == 0 and cards[card].color == "Red") or (gameState.turnTracker == 1 and cards[card].color == "Blue")) and "TRUE" or "FALSE" 1247 | for color,_ in pairs(votes[gameState.turnTracker]) do 1248 | if Player[color].seated then 1249 | players = players .. ((players == "") and Player[color].steam_id or (',' .. Player[color].steam_id)) 1250 | end 1251 | end 1252 | api_clueGuess(players, cards[card].id, correct, cards[card].color:upper()) 1253 | 1254 | local messageColor = { 1255 | ["Red"] = "da1918", 1256 | ["Blue"] = "1f87ff", 1257 | ["White"] = "ffffff", 1258 | ["Black"] = "191919" 1259 | } 1260 | 1261 | if correct == "TRUE" then 1262 | -- Play the correct sound effect 1263 | tableObject.AssetBundle.playTriggerEffect(0) 1264 | printToAll("[a020f0]» [31b32b][✓] [" .. messageColor[cards[card].color] .. "]" .. cards[card].color:upper() .. " [ffffff]team has correctly guessed: [" .. messageColor[cards[card].color] .."]" .. cards[card].value:upper() .. " [a020f0]«") 1265 | else 1266 | -- Play the wrong sound effect 1267 | tableObject.AssetBundle.playTriggerEffect(1) 1268 | local guessingTeam = gameState.turnTracker == 0 and "Red" or "Blue" 1269 | printToAll("[a020f0]» [da1918][✗] [" .. messageColor[guessingTeam] .. "]" .. guessingTeam:upper() .. " [ffffff]team has incorrectly guessed: [" .. messageColor[cards[card].color] .."]" .. cards[card].value:upper() .. " [a020f0]«") 1270 | end 1271 | 1272 | -- Check to see if either red or blue won 1273 | local redWon = true 1274 | local blueWon = true 1275 | for i = 1, 25, 1 do 1276 | if cards[i].color == "Red" and not cards[i].covered then 1277 | redWon = false 1278 | elseif cards[i].color == "Blue" and not cards[i].covered then 1279 | blueWon = false 1280 | end 1281 | 1282 | if not redWon and not blueWon then 1283 | -- Skip unnecessary iterations 1284 | break 1285 | end 1286 | end 1287 | 1288 | if cards[card].color == "Black" or blueWon or redWon then 1289 | -- End game scenario 1290 | 1291 | if (cards[card].color == "Black" and gameState.turnTracker == 0) or blueWon then 1292 | -- Red placed black card, blue wins 1293 | -- or blue placed all of their cards 1294 | broadcastToAll("[a020f0]» [1f87ff]BLUE [ffffff]team wins! [a020f0]«") 1295 | api_gameEnd("BLUE") 1296 | elseif (cards[card].color == "Black" and gameState.turnTracker == 1) or redWon then 1297 | -- Blue placed black card, red wins 1298 | -- or red placed all of their cards 1299 | broadcastToAll("[a020f0]» [da1918]RED [ffffff]team wins! [a020f0]«") 1300 | api_gameEnd("RED") 1301 | end 1302 | 1303 | -- Move the remaining agents to their codes 1304 | endGame() 1305 | 1306 | elseif gameState.guessesLeft == 0 or cards[card].color == "White" or (cards[card].color == "Red" and gameState.turnTracker == 1) or (cards[card].color == "Blue" and gameState.turnTracker == 0) then 1307 | toggleTurns() 1308 | else 1309 | -- Show the pass turn button and change color 1310 | tableObject.UI.setAttributes("passTurn", { 1311 | color = gameState.turnTracker == 0 and "#da1918" or "#1f87ff", 1312 | active = true 1313 | }) 1314 | end 1315 | end 1316 | end 1317 | 1318 | function coverCard(cardIndex, agentGUID) 1319 | local cardObject = getObjectFromGUID(cards[cardIndex].guid) 1320 | for guid, agentData in pairs(agents) do 1321 | local agentObject = getObjectFromGUID(guid) 1322 | if agents[guid].enabled and agentData.color == cards[cardIndex].color and agentObject and agentObject.interactable and (agentGUID == nil or agentGUID == guid) then 1323 | local position = {cards[cardIndex].position.x, 1.03, cards[cardIndex].position.z} 1324 | 1325 | -- Track the index of the card this agent is covering 1326 | agents[guid].covering = cardIndex 1327 | 1328 | -- Lock both cards and set them in place 1329 | cardObject.interactable = false 1330 | agentObject.interactable = false 1331 | 1332 | cardObject.setLock(true) 1333 | agentObject.setLock(true) 1334 | 1335 | -- Ensure the tile is on the code 1336 | cardObject.setRotationSmooth({0, 180, 0}) 1337 | agentObject.setRotationSmooth({0, 180, 180}) 1338 | 1339 | cardObject.setPositionSmooth(position) 1340 | agentObject.setPositionSmooth(position) 1341 | break 1342 | end 1343 | end 1344 | end 1345 | 1346 | function updateVoteUI(card) 1347 | local uiObject = card == 26 and tableObject or getObjectFromGUID(cards[card].guid) 1348 | 1349 | local votesToAdd = {} 1350 | for playerColor, voteData in pairs(votes[gameState.turnTracker]) do 1351 | if voteData == card then 1352 | votesToAdd[playerColor] = true 1353 | end 1354 | end 1355 | local votesOnCard = 1356 | { 1357 | uiObject.UI.getAttribute("vote-1", "color"), 1358 | uiObject.UI.getAttribute("vote-2", "color"), 1359 | uiObject.UI.getAttribute("vote-3", "color"), 1360 | uiObject.UI.getAttribute("vote-4", "color") 1361 | } 1362 | local newVotes = {} 1363 | 1364 | -- Keep the order of the current votes 1365 | for voteIndex, voteColor in ipairs(votesOnCard) do 1366 | if voteColor ~= "Black" then 1367 | if votes[gameState.turnTracker][voteColor] == card then 1368 | table.insert(newVotes, voteColor) 1369 | votesToAdd[voteColor] = nil 1370 | end 1371 | else 1372 | -- Add the uncounted votes 1373 | for playerColor,_ in pairs(votesToAdd) do 1374 | table.insert(newVotes, playerColor) 1375 | end 1376 | break 1377 | end 1378 | end 1379 | 1380 | for i = 1,4,1 do 1381 | if newVotes[i] ~= nil then 1382 | uiObject.UI.setAttributes("vote-" .. i, { 1383 | color = newVotes[i], 1384 | active = true 1385 | }) 1386 | else 1387 | uiObject.UI.setAttributes("vote-" .. i, { 1388 | color = "Black", 1389 | active = false 1390 | }) 1391 | end 1392 | end 1393 | end 1394 | 1395 | function votePass(color) 1396 | playerVote(color, 26) 1397 | end 1398 | 1399 | function onObjectPickUp(color, object) 1400 | -- Clues 1401 | if object.tag == "Card" and not object.spawning then 1402 | for i = 1, 25, 1 do 1403 | if cards[i].guid == object.guid then 1404 | object.setVelocity({0, 0, 0}) 1405 | object.drop() 1406 | object.setPosition({cards[i].position.x, 1.03, cards[i].position.z}) 1407 | if #Player[color].getSelectedObjects() <= 1 then 1408 | playerVote(color, i) 1409 | end 1410 | break 1411 | end 1412 | end 1413 | elseif agents[object.guid] ~= nil then 1414 | -- Prevent picking up of agent tiles from anyone but red or blue 1415 | if color ~= "Red" and color ~= "Blue" then 1416 | object.setVelocity({0, 0, 0}) 1417 | object.drop() 1418 | object.setPosition({agents[object.guid].position.x, 1.01, agents[object.guid].position.z}) 1419 | end 1420 | end 1421 | end 1422 | 1423 | function onPlayerAction(player, action, objects) 1424 | local processAction = false 1425 | local actionsToProcess = { 1426 | Player.Action.PickUp, 1427 | Player.Action.RotateIncrementalLeft, 1428 | Player.Action.RotateIncrementalRight, 1429 | Player.Action.RotateOver, 1430 | Player.Action.FlipIncrementalLeft, 1431 | Player.Action.FlipIncrementalRight, 1432 | Player.Action.FlipOver 1433 | } 1434 | 1435 | for _, handledAction in ipairs(actionsToProcess) do 1436 | if action == handledAction then 1437 | processAction = true 1438 | break 1439 | end 1440 | end 1441 | 1442 | if not processAction then 1443 | return 1444 | end 1445 | 1446 | local objectIncludesCard = false 1447 | local objectIncludesAgent = false 1448 | 1449 | -- See if there is a card included in the table of objects 1450 | for _, object in ipairs(objects) do 1451 | if isCard(object.guid) then 1452 | objectIncludesCard = true 1453 | elseif agents[object.guid] ~= nil then 1454 | objectIncludesAgent = true 1455 | end 1456 | end 1457 | 1458 | if objectIncludesCard and (player.color == "Blue" or player.color == "Red" or player.color == "Black") then 1459 | player.broadcast("[a020f0]» [da1918]ERROR: [ffffff]You're not allowed to do that! [a020f0]«") 1460 | return false 1461 | end 1462 | 1463 | -- Disable interacting with agents if player isn't a codemaster 1464 | if objectIncludesAgent and player.color ~= "Blue" and player.color ~= "Red" then 1465 | player.broadcast("[a020f0]» [da1918]ERROR: [ffffff]You're not allowed to do that! [a020f0]«") 1466 | return false 1467 | end 1468 | 1469 | local isPlayerTurn = (votes[gameState.turnTracker][player.color] ~= nil) 1470 | local errorMessage 1471 | 1472 | if action == Player.Action.PickUp then 1473 | if objectIncludesCard and not isPlayerTurn then 1474 | errorMessage = "[a020f0]» [da1918]ERROR: [ffffff]It's not your turn to vote! [a020f0]«" 1475 | elseif settings.cardTilting and gameState.status == 1 and not gameState.canVote then 1476 | errorMessage = "[a020f0]» [da1918]ERROR: [ffffff]You can't vote until you've been given a clue! [a020f0]«" 1477 | end 1478 | elseif action == Player.Action.RotateIncrementalLeft or action == Player.Action.RotateIncrementalRight or action == Player.Action.RotateOver then 1479 | if objectIncludesCard and not isPlayerTurn then 1480 | errorMessage = "[a020f0]» [da1918]ERROR: [ffffff]You can't tilt cards when it's not your turn! [a020f0]«" 1481 | elseif settings.cardTilting and gameState.status == 1 and not gameState.canVote then 1482 | errorMessage = "[a020f0]» [da1918]ERROR: [ffffff]You can't tilt cards until you've been given a clue! [a020f0]«" 1483 | end 1484 | elseif action == Player.Action.FlipIncrementalLeft or action == Player.Action.FlipIncrementalRight or action == Player.Action.FlipOver then 1485 | if objectIncludesCard and not isPlayerTurn then 1486 | errorMessage = "[a020f0]» [da1918]ERROR: [ffffff]You can't flip cards when it's not your turn! [a020f0]«" 1487 | elseif settings.cardTilting and gameState.status == 1 and not gameState.canVote then 1488 | errorMessage = "[a020f0]» [da1918]ERROR: [ffffff]You can't flip cards until you've been given a clue! [a020f0]«" 1489 | end 1490 | end 1491 | 1492 | if errorMessage ~= nil then 1493 | player.broadcast(errorMessage, redColor) 1494 | return false 1495 | end 1496 | end 1497 | 1498 | function isCard(guid) 1499 | for _, cardData in ipairs(cards) do 1500 | if guid == cardData.guid then 1501 | return true 1502 | end 1503 | end 1504 | return false 1505 | end 1506 | 1507 | function endGame() 1508 | -- Disable voting for any teams 1509 | gameState.canVote = false 1510 | 1511 | -- Set the game back to initial state 1512 | gameState.status = -1 1513 | 1514 | -- Stop the timer 1515 | tableObject.call("stopTimer", true) 1516 | 1517 | -- Hide the pass turn button and change color 1518 | tableObject.UI.setAttributes("passTurn", { 1519 | color = "#aaaaaa", 1520 | active = false 1521 | }) 1522 | 1523 | -- Assign remaining agents to uncovered clues 1524 | for i = 1, 25, 1 do 1525 | if not cards[i].covered then 1526 | coverCard(i, nil) 1527 | end 1528 | end 1529 | end 1530 | 1531 | function timeExpired() 1532 | -- Play the timer expired sound 1533 | tableObject.AssetBundle.playTriggerEffect(2) 1534 | 1535 | toggleTurns() 1536 | end 1537 | 1538 | -- Command implementations 1539 | function onChat(message, player) 1540 | if not player.admin then 1541 | return 1542 | end 1543 | 1544 | local command, color = processChat(message) 1545 | local ranCommand = false 1546 | 1547 | if command == nil then 1548 | return 1549 | end 1550 | 1551 | if command == "!kick" then 1552 | if not isColor(color) then 1553 | player.broadcast("Invalid color!", redColor) 1554 | return false 1555 | end 1556 | 1557 | if not Player[color].seated then 1558 | player.broadcast("There's no player seated in " .. color:lower() .. "!", redColor) 1559 | return false 1560 | end 1561 | printToAll(player.steam_name .. " has kicked " .. Player[color].steam_name .. ".", redColor) 1562 | Player[color].kick() 1563 | ranCommand = true 1564 | elseif command == "!blind" then 1565 | if not isColor(color) then 1566 | player.broadcast("Invalid color!", redColor) 1567 | return false 1568 | end 1569 | 1570 | if not Player[color].seated then 1571 | player.broadcast("There's no player seated in " .. color:lower() .. "!", redColor) 1572 | return false 1573 | end 1574 | printToAll(player.steam_name .. " has " .. (not Player[color].blindfolded and "blindfolded " or "unblindfolded ") .. Player[color].steam_name .. ".", redColor) 1575 | Player[color].blindfolded = not Player[color].blindfolded 1576 | ranCommand = true 1577 | elseif command == "!stand" then 1578 | if not isColor(color) then 1579 | player.broadcast("Invalid color!", redColor) 1580 | return false 1581 | end 1582 | 1583 | if not Player[color].seated then 1584 | player.broadcast("There's no player seated in " .. color:lower() .. "!", redColor) 1585 | return false 1586 | end 1587 | printToAll(player.steam_name .. " has stood " .. Player[color].steam_name .. ".", redColor) 1588 | Player[color].changeColor("Grey") 1589 | ranCommand = true 1590 | end 1591 | 1592 | if ranCommand then 1593 | return false 1594 | end 1595 | end 1596 | 1597 | function isColor(userColor) 1598 | for _, color in ipairs(Player.getColors()) do 1599 | if color ~= "Grey" and color:lower() == userColor:lower() then 1600 | return true 1601 | end 1602 | end 1603 | return false 1604 | end 1605 | 1606 | function processChat(message) 1607 | local raw = string.gmatch(message, "%S+") 1608 | local firstWord = true 1609 | local command = nil 1610 | local args = "" 1611 | for word in raw do 1612 | if firstWord then 1613 | if word:sub(1, 1) ~= "!" then 1614 | return command, args 1615 | else 1616 | command = word 1617 | firstWord = false 1618 | end 1619 | else 1620 | args = args .. word .. " " 1621 | end 1622 | end 1623 | 1624 | return command, args:gsub("^%s*(.-)%s*$", "%1") 1625 | end 1626 | 1627 | function toggleTurns() 1628 | -- Diable voting for the current team 1629 | gameState.canVote = false 1630 | 1631 | -- Remove all vote indicators and reset votes 1632 | local cardsToClear = {} 1633 | for playerColor, voteData in pairs(votes[gameState.turnTracker]) do 1634 | -- Update the vote UI first 1635 | if voteData ~= 0 then 1636 | table.insert(cardsToClear, voteData) 1637 | votes[gameState.turnTracker][playerColor] = 0 1638 | end 1639 | end 1640 | 1641 | for _, card in ipairs(cardsToClear) do 1642 | updateVoteUI(card) 1643 | end 1644 | 1645 | -- Disable card tilting 1646 | for _, card in ipairs(cards) do 1647 | if not card.covered then 1648 | local cardObject = getObjectFromGUID(card.guid) 1649 | if settings.cardTilting then 1650 | cardObject.setLock(true) 1651 | end 1652 | cardObject.setRotationSmooth({0, 180, 0}) 1653 | end 1654 | end 1655 | 1656 | -- Hide the pass turn button 1657 | tableObject.UI.setAttributes("passTurn", { 1658 | color = "#aaaaaa", 1659 | active = false 1660 | }) 1661 | 1662 | -- Reset the timer warning 1663 | timerWarning = -1 1664 | 1665 | -- Make sure it isn't the first turn 1666 | gameState.firstTurn = false 1667 | 1668 | -- Reset the guesses each team has left 1669 | gameState.guessesLeft = -1 1670 | 1671 | -- Reset the clueID 1672 | analytics.clueID = nil 1673 | 1674 | -- Play the button press effect 1675 | local button = (gameState.turnTracker == 0 and buttonRed or buttonBlue) 1676 | button.AssetBundle.playTriggerEffect(0) 1677 | 1678 | -- Change the tracker to the other team's turn 1679 | gameState.turnTracker = (gameState.turnTracker == 0 and 1 or 0) 1680 | 1681 | -- Change the location of the turn marker 1682 | local previousTurn = (gameState.turnTracker == 0 and "blue" or "red") .. "Turn" 1683 | local currentTurn = (gameState.turnTracker == 0 and "red" or "blue") .. "Turn" 1684 | local teamColorTurn = gameState.turnTracker == 0 and "Red" or "Blue" 1685 | tableObject.UI.setAttribute(previousTurn, "active", false) 1686 | tableObject.UI.setAttribute(currentTurn, "active", true) 1687 | tableObject.UI.setAttribute(previousTurn .. "BG", "active", false) 1688 | tableObject.UI.setAttribute(currentTurn .. "BG", "active", true) 1689 | tableObject.UI.setAttribute("timer", "outline", teamColorTurn) 1690 | printToAll("[a020f0]» " .. (teamColorTurn == "Red" and "[da1918]RED" or "[1f87ff]BLUE") .. " [ffffff]team's turn! [a020f0]«") 1691 | 1692 | startTime() 1693 | end 1694 | 1695 | function endTurn(button, color) 1696 | if color == "Red" or color == "Blue" then 1697 | -- Is it the current side's turn? 1698 | if gameState.turnTracker == 0 and button.guid == buttonBlue.guid or gameState.turnTracker == 1 and button.guid == buttonRed.guid then 1699 | return 1700 | end 1701 | 1702 | toggleTurns() 1703 | 1704 | end 1705 | end 1706 | 1707 | function updateDecks(response) 1708 | local totalDecks = response["decks"]["totalDecks"] 1709 | local deckList = response["decks"]["deckList"] 1710 | 1711 | deck.totalDecks = tonumber(totalDecks) 1712 | 1713 | for i = 1, 20, 1 do 1714 | if deckList[i] then 1715 | tableObject.UI.setAttribute("deck" .. i .. "Button", "onClick", "setDeck(" .. deckList[i]["deckID"] .. ")") 1716 | -- Deck image 1717 | if deckList[i]["author"]["steamID"] == nil then 1718 | tableObject.UI.setAttribute("deck" .. i .. "Official", "active", true) 1719 | tableObject.UI.setAttribute("deck" .. i .. "Image", "active", false) 1720 | elseif deckList[i]["author"]["profilePicture"] then 1721 | tableObject.UI.setAttribute("deck" .. i .. "Official", "active", false) 1722 | local assets = UI.getCustomAssets() 1723 | 1724 | local addCustomAsset = true 1725 | for _,asset in ipairs(assets) do 1726 | if asset["name"] == deckList[i]["author"]["steamID"] then 1727 | addCustomAsset = false 1728 | break 1729 | end 1730 | end 1731 | if addCustomAsset then 1732 | UI.setCustomAssets(table.insert(assets, { 1733 | name = deckList[i]["author"]["steamID"], 1734 | url = deckList[i]["author"]["profilePicture"]:gsub(".jpg", "_full.jpg") 1735 | })) 1736 | end 1737 | tableObject.UI.setAttributes("deck" .. i .. "Image", { 1738 | active = true, 1739 | image = deckList[i]["author"]["steamID"] 1740 | }) 1741 | end 1742 | 1743 | tableObject.UI.setAttributes("deck" .. i .. "Name", { 1744 | text = deckList[i]["name"] 1745 | }) 1746 | tableObject.UI.setAttributes("deck" .. i .. "Author", { 1747 | text = (deckList[i]["author"]["displayName"] and ("by " .. deckList[i]["author"]["displayName"]) or "Official Deck") 1748 | }) 1749 | tableObject.UI.setAttributes("deck" .. i .. "Description", { 1750 | text = deckList[i]["description"], 1751 | active = (deckList[i]["description"] == nil and "false" or "true") 1752 | }) 1753 | 1754 | tableObject.UI.setAttribute("deck" .. i .. "Button", "color", "#ffffff") 1755 | for deckIndex, deckGUID in ipairs(deck.selected) do 1756 | if deckGUID == deckList[i]["deckID"] then 1757 | tableObject.UI.setAttribute("deck" .. i .. "Button", "color", "#aaeaa7") 1758 | break 1759 | end 1760 | end 1761 | 1762 | tableObject.UI.setAttribute("deck" .. i, "active", true) 1763 | else 1764 | tableObject.UI.setAttribute("deck" .. i, "active", false) 1765 | end 1766 | end 1767 | tableObject.UI.setValue("pageMarker", "Page: " .. deck.pageNum + 1 .. " / " .. math.ceil(deck.totalDecks / deck.pageSize)) 1768 | deck.fetchingDecks = false 1769 | end 1770 | 1771 | ------------------------------------------------------------------------------- 1772 | --------------------------------[[ ANALYTICS ]]-------------------------------- 1773 | ------------------------------------------------------------------------------- 1774 | 1775 | function apiGet(api, callback) 1776 | WebRequest.get(analytics.url .. api, callback) 1777 | end 1778 | 1779 | function apiPost(api, payload, callback) 1780 | WebRequest.post(analytics.url .. api, payload, callback) 1781 | end 1782 | 1783 | function getJSON(responseRaw) 1784 | if not responseRaw.is_done then 1785 | -- Don't process until the response is ready 1786 | return nil 1787 | end 1788 | 1789 | local response = JSON.decode(responseRaw.text) 1790 | 1791 | if response.status == "error" then 1792 | print("API Error: " .. response.message) 1793 | return nil 1794 | end 1795 | 1796 | return response 1797 | end 1798 | 1799 | -------------- /games/decks -------------- 1800 | function api_getDecks() 1801 | deck.fetchingDecks = true 1802 | apiGet("/games/decks?pageSize=" .. deck.pageSize .. "&pageNum=" .. deck.pageNum .. (deck.searchTerm and ("&searchTerm=" .. deck.searchTerm) or ""), api_getDecksCB) 1803 | end 1804 | 1805 | function api_getDecksCB(responseRaw) 1806 | local response = getJSON(responseRaw) 1807 | if response == nil then 1808 | return 1809 | end 1810 | 1811 | -- Update the deck selector 1812 | updateDecks(response) 1813 | end 1814 | 1815 | -------------- /games/player/blacklisted/:steamID -------------- 1816 | function api_getBlacklistInfo(steamID) 1817 | apiGet("/games/player/blacklisted/" .. steamID, api_getBlacklistInfoCB) 1818 | end 1819 | 1820 | function api_getBlacklistInfoCB(responseRaw) 1821 | local response = getJSON(responseRaw) 1822 | if response == nil then 1823 | return 1824 | end 1825 | 1826 | if response.dateAdded ~= nil then 1827 | if settings.automod.autokick then 1828 | --player.kick() 1829 | end 1830 | --[[ 1831 | printToAll("\n", {1, 1, 1}) 1832 | printToAll(player.steam_name .. " was found in the global Codenames blacklist!", redColor) 1833 | printToAll("SteamID : " .. player.steam_id, redColor) 1834 | printToAll("Date Added : " .. response.dateAdded, redColor) 1835 | printToAll("Reason : " .. response.reason, redColor) 1836 | printToAll("\n", {1, 1, 1})]] 1837 | end 1838 | end 1839 | 1840 | -------------- /games/start -------------- 1841 | function api_gameStart() 1842 | local request = 1843 | { 1844 | host = analytics.host, 1845 | decks = "", 1846 | first = gameState.turnTracker == 0 and "RED" or "BLUE", 1847 | players = "" 1848 | } 1849 | 1850 | for deckIndex, deck in ipairs(deck.selected) do 1851 | if deckIndex == 1 then 1852 | request.decks = request.decks .. deck 1853 | else 1854 | request.decks = request.decks .. "," .. deck 1855 | end 1856 | end 1857 | 1858 | for _, player in ipairs(Player.getPlayers()) do 1859 | local color = player.color 1860 | if color ~= "Grey" and color ~= "Black" then 1861 | request.players = request.players .. ((request.players == "") and player.steam_id or (',' .. player.steam_id)) 1862 | end 1863 | end 1864 | 1865 | apiPost("/games/start", request, api_gameStartCB) 1866 | end 1867 | 1868 | function api_gameStartCB(responseRaw) 1869 | local response = getJSON(responseRaw) 1870 | if response == nil then 1871 | return 1872 | end 1873 | 1874 | -- Set the current game ID 1875 | analytics.gameID = response.gameID 1876 | 1877 | -- Reset player sessions 1878 | analytics.sessions = {} 1879 | for _, session in ipairs(response.sessions) do 1880 | analytics.sessions[session.steamID] = session.sessionID 1881 | end 1882 | 1883 | -- Set the words for the current deck 1884 | deck.words = response.words 1885 | 1886 | -- Reset the previous clue ID 1887 | analytics.clueID = nil 1888 | 1889 | -- Continue setup of the game 1890 | setupGame() 1891 | end 1892 | 1893 | -------------- /games/end -------------- 1894 | function api_gameEnd(winner) 1895 | local request = 1896 | { 1897 | gameID = analytics.gameID, 1898 | winner = winner 1899 | } 1900 | 1901 | apiPost("/games/end", request, api_gameEndCB) 1902 | end 1903 | 1904 | function api_gameEndCB(responseRaw) 1905 | local response = getJSON(responseRaw) 1906 | if response == nil then 1907 | return 1908 | end 1909 | 1910 | -- Set the current game ID 1911 | analytics.gameID = nil 1912 | 1913 | -- Reset player sessions 1914 | analytics.sessions = {} 1915 | 1916 | -- Reset the clue ID 1917 | analytics.clueID = nil 1918 | end 1919 | 1920 | -------------- /games/clues/new -------------- 1921 | function api_newClue(clue, number, codemaster) 1922 | local request = 1923 | { 1924 | gameID = analytics.gameID, 1925 | clue = tostring(clue), 1926 | number = tostring(number), 1927 | codemaster = tostring(codemaster), 1928 | team = tostring(gameState.turnTracker == 0 and "RED" or "BLUE") 1929 | } 1930 | 1931 | apiPost("/games/clues/new", request, api_newClueCB) 1932 | end 1933 | 1934 | function api_newClueCB(responseRaw) 1935 | local response = getJSON(responseRaw) 1936 | if response == nil then 1937 | return 1938 | end 1939 | 1940 | analytics.clueID = response.clueID 1941 | end 1942 | 1943 | -------------- /games/clues/guess -------------- 1944 | function api_clueGuess(players, guess, correct, color) 1945 | local request = 1946 | { 1947 | clueID = analytics.clueID, 1948 | players = players, 1949 | guess = guess, 1950 | correct = correct, 1951 | color = color 1952 | } 1953 | 1954 | apiPost("/games/clues/guess", request, api_clueGuessCB) 1955 | end 1956 | 1957 | function api_clueGuessCB(responseRaw) 1958 | local response = getJSON(responseRaw) 1959 | if response == nil then 1960 | return 1961 | end 1962 | end 1963 | 1964 | -------------- /games/players/sessions/start -------------- 1965 | function api_playerSessionStart(player) 1966 | local request = 1967 | { 1968 | gameID = analytics.gameID, 1969 | player = player 1970 | } 1971 | 1972 | apiPost("/games/players/sessions/start", request, api_playerSessionStartCB) 1973 | end 1974 | 1975 | function api_playerSessionStartCB(responseRaw) 1976 | local response = getJSON(responseRaw) 1977 | if response == nil then 1978 | return 1979 | end 1980 | 1981 | -- Record the session ID 1982 | analytics.sessions[response.steamID] = response.sessionID 1983 | end 1984 | 1985 | -------------- /games/players/sessions/end -------------- 1986 | function api_playerSessionEnd(sessionID) 1987 | local request = 1988 | { 1989 | sessionID = sessionID 1990 | } 1991 | 1992 | apiPost("/games/players/sessions/end", request, api_playerSessionEndCB) 1993 | end 1994 | 1995 | function api_playerSessionEndCB(responseRaw) 1996 | local response = getJSON(responseRaw) 1997 | if response == nil then 1998 | return 1999 | end 2000 | 2001 | -- Remove the session ID 2002 | for steamID, sessionID in pairs(analytics.sessions) do 2003 | if sessionID == response.sessionID then 2004 | analytics.sessions[steamID] = nil 2005 | end 2006 | end 2007 | end -------------------------------------------------------------------------------- /src/Global.-1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | 12 | 13 | ? 18 | 19 | 20 | 21 | 28 | 29 | 30 | 31 | 32 | 40 | 41 | 42 | ? 47 | 48 | 49 | 50 | 57 | 58 | 59 | 60 | 61 | --------------------------------------------------------------------------------