commit 1bcd6e968941c0994d273a14380302232dd70f1e Author: Tim Evans Date: Tue Jul 14 14:50:07 2026 +0100 SLA Zero Two v2.0.0 — fork of Zero Engine v1.7.6 as standalone slazero2 system Full identifier rename (id, paths, flags, packs), KickStart drugs moved into drugs.json source, legacy Blade Runner files removed, fresh history. Co-Authored-By: Claude Fable 5 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7493d2d --- /dev/null +++ b/.gitignore @@ -0,0 +1,32 @@ +# ── Foundry VTT system .gitignore ────────────────────────────────────────── + +# OS +.DS_Store +Thumbs.db +*.swp +*~ + +# Node / build artefacts (if ever added) +node_modules/ +dist/ +.env + +# Foundry local cache / temp files +*.lock +.fvtt-*/ + +# LevelDB lock files — created at runtime, must not be shipped in the zip +**/LOCK + +# Compiled / minified outputs (keep source only) +*.min.js +*.min.css + +# Editor +.vscode/ +.idea/ +*.sublime-project +*.sublime-workspace + +# Test output +coverage/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..0c4d27d --- /dev/null +++ b/LICENSE @@ -0,0 +1,688 @@ + COPYRIGHT + Copyright (C) Free League Developer Community 2022 + +All source code (javascript, hbs, scss, as well as system templates +and the like), are Copyright (C) 2022 Free League Developer Community, +and licenced under the GNU General Public License v3.0. + +The project is created following the Foundry VTT Limited License +Agreement for module development. + +The fonts used in this project carry their own licenses. + + SOFTWARE LICENSE + + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + +Copyright (C) 2007 Free Software Foundation, Inc. +Everyone is permitted to copy and distribute verbatim copies +of this license document, but changing it is not allowed. + + Preamble + +The GNU General Public License is a free, copyleft license for +software and other kinds of works. + +The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + +When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + +To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + +For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + +Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + +For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + +Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + +Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + +The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + +0. Definitions. + +"This License" refers to version 3 of the GNU General Public License. + +"Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + +"The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + +To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + +A "covered work" means either the unmodified Program or a work based +on the Program. + +To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + +To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + +An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + +1. Source Code. + +The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + +A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + +The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + +The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + +The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + +The Corresponding Source for a work in source code form is that +same work. + +2. Basic Permissions. + +All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + +You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + +Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + +3. Protecting Users' Legal Rights From Anti-Circumvention Law. + +No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + +When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + +4. Conveying Verbatim Copies. + +You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + +You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + +5. Conveying Modified Source Versions. + +You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + +A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + +6. Conveying Non-Source Forms. + +You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + +A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + +A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + +"Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + +If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + +The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + +Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + +7. Additional Terms. + +"Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + +When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + +Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + +All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + +If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + +Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + +8. Termination. + +You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + +However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + +Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + +Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + +9. Acceptance Not Required for Having Copies. + +You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + +10. Automatic Licensing of Downstream Recipients. + +Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + +An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + +You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + +11. Patents. + +A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + +A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + +Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + +In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + +If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + +If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + +A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + +Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + +12. No Surrender of Others' Freedom. + +If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + +13. Use with the GNU Affero General Public License. + +Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + +14. Revised Versions of this License. + +The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + +If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + +Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + +15. Disclaimer of Warranty. + +THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +16. Limitation of Liability. + +IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + +17. Interpretation of Sections 15 and 16. + +If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + +If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + +To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + +If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + +You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + +The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/README.md b/README.md new file mode 100644 index 0000000..c75b70b --- /dev/null +++ b/README.md @@ -0,0 +1,21 @@ +# SLA Zero Two + +**The sequel system for SLA Industries on Foundry VTT.** + +SLA Zero Two (`slazero2`) carries forward every mechanic of Zero Engine v1.7.6 — dice pools, Stress dice, Push, Panic, conditions, the Ebb, the Bullet Tax, the full GM toolset — wrapped in a completely rebuilt presentation: custom 3D dice, redesigned character sheets, and cinematic chat cards. + +It is a standalone system with its own id. It does not share worlds, actors, or settings with the original `zero-engine` system; both can be installed side by side. + +## Install + +Manifest URL: +``` +https://raw.githubusercontent.com/slaguru666/slazero2/main/system.json +``` + +Foundry v13+ (verified on v14). + +## Development + +- Everything lives in `slazero2.mjs`; styles in `styles/slazero2.css`. +- Compendium sources are the JSON files in `packs/`; build the LevelDB packs with `node build-packs.mjs` (requires `npm install` once). diff --git a/assets/SLA_Assets/Ammo/10-gauge-he__AP.png b/assets/SLA_Assets/Ammo/10-gauge-he__AP.png new file mode 100644 index 0000000..c225722 Binary files /dev/null and b/assets/SLA_Assets/Ammo/10-gauge-he__AP.png differ diff --git a/assets/SLA_Assets/Ammo/10-gauge-he__HE.png b/assets/SLA_Assets/Ammo/10-gauge-he__HE.png new file mode 100644 index 0000000..40ab7aa Binary files /dev/null and b/assets/SLA_Assets/Ammo/10-gauge-he__HE.png differ diff --git a/assets/SLA_Assets/Ammo/10-gauge-he__HEAP.png b/assets/SLA_Assets/Ammo/10-gauge-he__HEAP.png new file mode 100644 index 0000000..7213641 Binary files /dev/null and b/assets/SLA_Assets/Ammo/10-gauge-he__HEAP.png differ diff --git a/assets/SLA_Assets/Ammo/10-gauge-he__STD.png b/assets/SLA_Assets/Ammo/10-gauge-he__STD.png new file mode 100644 index 0000000..6657842 Binary files /dev/null and b/assets/SLA_Assets/Ammo/10-gauge-he__STD.png differ diff --git a/assets/SLA_Assets/Ammo/10-gauge__AP.png b/assets/SLA_Assets/Ammo/10-gauge__AP.png new file mode 100644 index 0000000..d93749d Binary files /dev/null and b/assets/SLA_Assets/Ammo/10-gauge__AP.png differ diff --git a/assets/SLA_Assets/Ammo/10-gauge__HE.png b/assets/SLA_Assets/Ammo/10-gauge__HE.png new file mode 100644 index 0000000..a5baea4 Binary files /dev/null and b/assets/SLA_Assets/Ammo/10-gauge__HE.png differ diff --git a/assets/SLA_Assets/Ammo/10-gauge__HEAP.png b/assets/SLA_Assets/Ammo/10-gauge__HEAP.png new file mode 100644 index 0000000..7a2e6b1 Binary files /dev/null and b/assets/SLA_Assets/Ammo/10-gauge__HEAP.png differ diff --git a/assets/SLA_Assets/Ammo/10-gauge__STD.png b/assets/SLA_Assets/Ammo/10-gauge__STD.png new file mode 100644 index 0000000..98378a3 Binary files /dev/null and b/assets/SLA_Assets/Ammo/10-gauge__STD.png differ diff --git a/assets/SLA_Assets/Ammo/10mm-pistol__AP.png b/assets/SLA_Assets/Ammo/10mm-pistol__AP.png new file mode 100644 index 0000000..7f7c324 Binary files /dev/null and b/assets/SLA_Assets/Ammo/10mm-pistol__AP.png differ diff --git a/assets/SLA_Assets/Ammo/10mm-pistol__HE.png b/assets/SLA_Assets/Ammo/10mm-pistol__HE.png new file mode 100644 index 0000000..58ef473 Binary files /dev/null and b/assets/SLA_Assets/Ammo/10mm-pistol__HE.png differ diff --git a/assets/SLA_Assets/Ammo/10mm-pistol__HEAP.png b/assets/SLA_Assets/Ammo/10mm-pistol__HEAP.png new file mode 100644 index 0000000..2ded85b Binary files /dev/null and b/assets/SLA_Assets/Ammo/10mm-pistol__HEAP.png differ diff --git a/assets/SLA_Assets/Ammo/10mm-pistol__STD.png b/assets/SLA_Assets/Ammo/10mm-pistol__STD.png new file mode 100644 index 0000000..4952131 Binary files /dev/null and b/assets/SLA_Assets/Ammo/10mm-pistol__STD.png differ diff --git a/assets/SLA_Assets/Ammo/10mm-rifle__AP.png b/assets/SLA_Assets/Ammo/10mm-rifle__AP.png new file mode 100644 index 0000000..b4a37d0 Binary files /dev/null and b/assets/SLA_Assets/Ammo/10mm-rifle__AP.png differ diff --git a/assets/SLA_Assets/Ammo/10mm-rifle__HE.png b/assets/SLA_Assets/Ammo/10mm-rifle__HE.png new file mode 100644 index 0000000..43305ae Binary files /dev/null and b/assets/SLA_Assets/Ammo/10mm-rifle__HE.png differ diff --git a/assets/SLA_Assets/Ammo/10mm-rifle__HEAP.png b/assets/SLA_Assets/Ammo/10mm-rifle__HEAP.png new file mode 100644 index 0000000..1a6a3ef Binary files /dev/null and b/assets/SLA_Assets/Ammo/10mm-rifle__HEAP.png differ diff --git a/assets/SLA_Assets/Ammo/10mm-rifle__STD.png b/assets/SLA_Assets/Ammo/10mm-rifle__STD.png new file mode 100644 index 0000000..b08973b Binary files /dev/null and b/assets/SLA_Assets/Ammo/10mm-rifle__STD.png differ diff --git a/assets/SLA_Assets/Ammo/10mm-smg__AP.png b/assets/SLA_Assets/Ammo/10mm-smg__AP.png new file mode 100644 index 0000000..4fb1651 Binary files /dev/null and b/assets/SLA_Assets/Ammo/10mm-smg__AP.png differ diff --git a/assets/SLA_Assets/Ammo/10mm-smg__HE.png b/assets/SLA_Assets/Ammo/10mm-smg__HE.png new file mode 100644 index 0000000..09dbe13 Binary files /dev/null and b/assets/SLA_Assets/Ammo/10mm-smg__HE.png differ diff --git a/assets/SLA_Assets/Ammo/10mm-smg__HEAP.png b/assets/SLA_Assets/Ammo/10mm-smg__HEAP.png new file mode 100644 index 0000000..8b86442 Binary files /dev/null and b/assets/SLA_Assets/Ammo/10mm-smg__HEAP.png differ diff --git a/assets/SLA_Assets/Ammo/10mm-smg__STD.png b/assets/SLA_Assets/Ammo/10mm-smg__STD.png new file mode 100644 index 0000000..b6fe4c3 Binary files /dev/null and b/assets/SLA_Assets/Ammo/10mm-smg__STD.png differ diff --git a/assets/SLA_Assets/Ammo/12-gauge-he__AP.png b/assets/SLA_Assets/Ammo/12-gauge-he__AP.png new file mode 100644 index 0000000..c69c00b Binary files /dev/null and b/assets/SLA_Assets/Ammo/12-gauge-he__AP.png differ diff --git a/assets/SLA_Assets/Ammo/12-gauge-he__HE.png b/assets/SLA_Assets/Ammo/12-gauge-he__HE.png new file mode 100644 index 0000000..052b603 Binary files /dev/null and b/assets/SLA_Assets/Ammo/12-gauge-he__HE.png differ diff --git a/assets/SLA_Assets/Ammo/12-gauge-he__HEAP.png b/assets/SLA_Assets/Ammo/12-gauge-he__HEAP.png new file mode 100644 index 0000000..8fe1a23 Binary files /dev/null and b/assets/SLA_Assets/Ammo/12-gauge-he__HEAP.png differ diff --git a/assets/SLA_Assets/Ammo/12-gauge-he__STD.png b/assets/SLA_Assets/Ammo/12-gauge-he__STD.png new file mode 100644 index 0000000..eeb88cd Binary files /dev/null and b/assets/SLA_Assets/Ammo/12-gauge-he__STD.png differ diff --git a/assets/SLA_Assets/Ammo/12-gauge__AP.png b/assets/SLA_Assets/Ammo/12-gauge__AP.png new file mode 100644 index 0000000..0ec2715 Binary files /dev/null and b/assets/SLA_Assets/Ammo/12-gauge__AP.png differ diff --git a/assets/SLA_Assets/Ammo/12-gauge__HE.png b/assets/SLA_Assets/Ammo/12-gauge__HE.png new file mode 100644 index 0000000..12f92c4 Binary files /dev/null and b/assets/SLA_Assets/Ammo/12-gauge__HE.png differ diff --git a/assets/SLA_Assets/Ammo/12-gauge__HEAP.png b/assets/SLA_Assets/Ammo/12-gauge__HEAP.png new file mode 100644 index 0000000..ec38832 Binary files /dev/null and b/assets/SLA_Assets/Ammo/12-gauge__HEAP.png differ diff --git a/assets/SLA_Assets/Ammo/12-gauge__STD.png b/assets/SLA_Assets/Ammo/12-gauge__STD.png new file mode 100644 index 0000000..a6bd177 Binary files /dev/null and b/assets/SLA_Assets/Ammo/12-gauge__STD.png differ diff --git a/assets/SLA_Assets/Ammo/12mm__AP.png b/assets/SLA_Assets/Ammo/12mm__AP.png new file mode 100644 index 0000000..94cd8eb Binary files /dev/null and b/assets/SLA_Assets/Ammo/12mm__AP.png differ diff --git a/assets/SLA_Assets/Ammo/12mm__HE.png b/assets/SLA_Assets/Ammo/12mm__HE.png new file mode 100644 index 0000000..e0cefe6 Binary files /dev/null and b/assets/SLA_Assets/Ammo/12mm__HE.png differ diff --git a/assets/SLA_Assets/Ammo/12mm__HEAP.png b/assets/SLA_Assets/Ammo/12mm__HEAP.png new file mode 100644 index 0000000..c223fdb Binary files /dev/null and b/assets/SLA_Assets/Ammo/12mm__HEAP.png differ diff --git a/assets/SLA_Assets/Ammo/12mm__STD.png b/assets/SLA_Assets/Ammo/12mm__STD.png new file mode 100644 index 0000000..ddaab10 Binary files /dev/null and b/assets/SLA_Assets/Ammo/12mm__STD.png differ diff --git a/assets/SLA_Assets/Ammo/20mm-hedp__AP.png b/assets/SLA_Assets/Ammo/20mm-hedp__AP.png new file mode 100644 index 0000000..7a50db2 Binary files /dev/null and b/assets/SLA_Assets/Ammo/20mm-hedp__AP.png differ diff --git a/assets/SLA_Assets/Ammo/20mm-hedp__HE.png b/assets/SLA_Assets/Ammo/20mm-hedp__HE.png new file mode 100644 index 0000000..bb94996 Binary files /dev/null and b/assets/SLA_Assets/Ammo/20mm-hedp__HE.png differ diff --git a/assets/SLA_Assets/Ammo/20mm-hedp__HEAP.png b/assets/SLA_Assets/Ammo/20mm-hedp__HEAP.png new file mode 100644 index 0000000..4e28398 Binary files /dev/null and b/assets/SLA_Assets/Ammo/20mm-hedp__HEAP.png differ diff --git a/assets/SLA_Assets/Ammo/20mm-hedp__STD.png b/assets/SLA_Assets/Ammo/20mm-hedp__STD.png new file mode 100644 index 0000000..323d1aa Binary files /dev/null and b/assets/SLA_Assets/Ammo/20mm-hedp__STD.png differ diff --git a/assets/SLA_Assets/Ammo/25mm-he__AP.png b/assets/SLA_Assets/Ammo/25mm-he__AP.png new file mode 100644 index 0000000..5438607 Binary files /dev/null and b/assets/SLA_Assets/Ammo/25mm-he__AP.png differ diff --git a/assets/SLA_Assets/Ammo/25mm-he__HE.png b/assets/SLA_Assets/Ammo/25mm-he__HE.png new file mode 100644 index 0000000..5fca8bf Binary files /dev/null and b/assets/SLA_Assets/Ammo/25mm-he__HE.png differ diff --git a/assets/SLA_Assets/Ammo/25mm-he__HEAP.png b/assets/SLA_Assets/Ammo/25mm-he__HEAP.png new file mode 100644 index 0000000..6d23afe Binary files /dev/null and b/assets/SLA_Assets/Ammo/25mm-he__HEAP.png differ diff --git a/assets/SLA_Assets/Ammo/25mm-he__STD.png b/assets/SLA_Assets/Ammo/25mm-he__STD.png new file mode 100644 index 0000000..eb60a4d Binary files /dev/null and b/assets/SLA_Assets/Ammo/25mm-he__STD.png differ diff --git a/assets/SLA_Assets/Ammo/30mm-heat__AP.png b/assets/SLA_Assets/Ammo/30mm-heat__AP.png new file mode 100644 index 0000000..de798e7 Binary files /dev/null and b/assets/SLA_Assets/Ammo/30mm-heat__AP.png differ diff --git a/assets/SLA_Assets/Ammo/30mm-heat__HE.png b/assets/SLA_Assets/Ammo/30mm-heat__HE.png new file mode 100644 index 0000000..f1fbda2 Binary files /dev/null and b/assets/SLA_Assets/Ammo/30mm-heat__HE.png differ diff --git a/assets/SLA_Assets/Ammo/30mm-heat__HEAP.png b/assets/SLA_Assets/Ammo/30mm-heat__HEAP.png new file mode 100644 index 0000000..4c1f31c Binary files /dev/null and b/assets/SLA_Assets/Ammo/30mm-heat__HEAP.png differ diff --git a/assets/SLA_Assets/Ammo/30mm-heat__STD.png b/assets/SLA_Assets/Ammo/30mm-heat__STD.png new file mode 100644 index 0000000..450518c Binary files /dev/null and b/assets/SLA_Assets/Ammo/30mm-heat__STD.png differ diff --git a/assets/SLA_Assets/Ammo/3mm-smg__AP.png b/assets/SLA_Assets/Ammo/3mm-smg__AP.png new file mode 100644 index 0000000..7b8e1a8 Binary files /dev/null and b/assets/SLA_Assets/Ammo/3mm-smg__AP.png differ diff --git a/assets/SLA_Assets/Ammo/3mm-smg__HE.png b/assets/SLA_Assets/Ammo/3mm-smg__HE.png new file mode 100644 index 0000000..f1703d4 Binary files /dev/null and b/assets/SLA_Assets/Ammo/3mm-smg__HE.png differ diff --git a/assets/SLA_Assets/Ammo/3mm-smg__HEAP.png b/assets/SLA_Assets/Ammo/3mm-smg__HEAP.png new file mode 100644 index 0000000..19a2638 Binary files /dev/null and b/assets/SLA_Assets/Ammo/3mm-smg__HEAP.png differ diff --git a/assets/SLA_Assets/Ammo/3mm-smg__STD.png b/assets/SLA_Assets/Ammo/3mm-smg__STD.png new file mode 100644 index 0000000..08026d1 Binary files /dev/null and b/assets/SLA_Assets/Ammo/3mm-smg__STD.png differ diff --git a/assets/SLA_Assets/Ammo/3mm__AP.png b/assets/SLA_Assets/Ammo/3mm__AP.png new file mode 100644 index 0000000..ade23a4 Binary files /dev/null and b/assets/SLA_Assets/Ammo/3mm__AP.png differ diff --git a/assets/SLA_Assets/Ammo/3mm__HE.png b/assets/SLA_Assets/Ammo/3mm__HE.png new file mode 100644 index 0000000..8787e87 Binary files /dev/null and b/assets/SLA_Assets/Ammo/3mm__HE.png differ diff --git a/assets/SLA_Assets/Ammo/3mm__HEAP.png b/assets/SLA_Assets/Ammo/3mm__HEAP.png new file mode 100644 index 0000000..140833d Binary files /dev/null and b/assets/SLA_Assets/Ammo/3mm__HEAP.png differ diff --git a/assets/SLA_Assets/Ammo/3mm__STD.png b/assets/SLA_Assets/Ammo/3mm__STD.png new file mode 100644 index 0000000..df914e7 Binary files /dev/null and b/assets/SLA_Assets/Ammo/3mm__STD.png differ diff --git a/assets/SLA_Assets/Ammo/40mm-he-grenade__AP.png b/assets/SLA_Assets/Ammo/40mm-he-grenade__AP.png new file mode 100644 index 0000000..a79496a Binary files /dev/null and b/assets/SLA_Assets/Ammo/40mm-he-grenade__AP.png differ diff --git a/assets/SLA_Assets/Ammo/40mm-he-grenade__HE.png b/assets/SLA_Assets/Ammo/40mm-he-grenade__HE.png new file mode 100644 index 0000000..18de395 Binary files /dev/null and b/assets/SLA_Assets/Ammo/40mm-he-grenade__HE.png differ diff --git a/assets/SLA_Assets/Ammo/40mm-he-grenade__HEAP.png b/assets/SLA_Assets/Ammo/40mm-he-grenade__HEAP.png new file mode 100644 index 0000000..b0f6a26 Binary files /dev/null and b/assets/SLA_Assets/Ammo/40mm-he-grenade__HEAP.png differ diff --git a/assets/SLA_Assets/Ammo/40mm-he-grenade__STD.png b/assets/SLA_Assets/Ammo/40mm-he-grenade__STD.png new file mode 100644 index 0000000..ddfab3d Binary files /dev/null and b/assets/SLA_Assets/Ammo/40mm-he-grenade__STD.png differ diff --git a/assets/SLA_Assets/Ammo/40mm-smoke-gas__AP.png b/assets/SLA_Assets/Ammo/40mm-smoke-gas__AP.png new file mode 100644 index 0000000..ab8c37a Binary files /dev/null and b/assets/SLA_Assets/Ammo/40mm-smoke-gas__AP.png differ diff --git a/assets/SLA_Assets/Ammo/40mm-smoke-gas__HE.png b/assets/SLA_Assets/Ammo/40mm-smoke-gas__HE.png new file mode 100644 index 0000000..8c806b5 Binary files /dev/null and b/assets/SLA_Assets/Ammo/40mm-smoke-gas__HE.png differ diff --git a/assets/SLA_Assets/Ammo/40mm-smoke-gas__HEAP.png b/assets/SLA_Assets/Ammo/40mm-smoke-gas__HEAP.png new file mode 100644 index 0000000..ba1caf8 Binary files /dev/null and b/assets/SLA_Assets/Ammo/40mm-smoke-gas__HEAP.png differ diff --git a/assets/SLA_Assets/Ammo/40mm-smoke-gas__STD.png b/assets/SLA_Assets/Ammo/40mm-smoke-gas__STD.png new file mode 100644 index 0000000..a572565 Binary files /dev/null and b/assets/SLA_Assets/Ammo/40mm-smoke-gas__STD.png differ diff --git a/assets/SLA_Assets/Ammo/5mm-rifle__AP.png b/assets/SLA_Assets/Ammo/5mm-rifle__AP.png new file mode 100644 index 0000000..b080739 Binary files /dev/null and b/assets/SLA_Assets/Ammo/5mm-rifle__AP.png differ diff --git a/assets/SLA_Assets/Ammo/5mm-rifle__HE.png b/assets/SLA_Assets/Ammo/5mm-rifle__HE.png new file mode 100644 index 0000000..193d7e6 Binary files /dev/null and b/assets/SLA_Assets/Ammo/5mm-rifle__HE.png differ diff --git a/assets/SLA_Assets/Ammo/5mm-rifle__HEAP.png b/assets/SLA_Assets/Ammo/5mm-rifle__HEAP.png new file mode 100644 index 0000000..81c17a5 Binary files /dev/null and b/assets/SLA_Assets/Ammo/5mm-rifle__HEAP.png differ diff --git a/assets/SLA_Assets/Ammo/5mm-rifle__STD.png b/assets/SLA_Assets/Ammo/5mm-rifle__STD.png new file mode 100644 index 0000000..1332dfd Binary files /dev/null and b/assets/SLA_Assets/Ammo/5mm-rifle__STD.png differ diff --git a/assets/SLA_Assets/Ammo/5mm-smg__AP.png b/assets/SLA_Assets/Ammo/5mm-smg__AP.png new file mode 100644 index 0000000..aa7fe08 Binary files /dev/null and b/assets/SLA_Assets/Ammo/5mm-smg__AP.png differ diff --git a/assets/SLA_Assets/Ammo/5mm-smg__HE.png b/assets/SLA_Assets/Ammo/5mm-smg__HE.png new file mode 100644 index 0000000..4baca1d Binary files /dev/null and b/assets/SLA_Assets/Ammo/5mm-smg__HE.png differ diff --git a/assets/SLA_Assets/Ammo/5mm-smg__HEAP.png b/assets/SLA_Assets/Ammo/5mm-smg__HEAP.png new file mode 100644 index 0000000..aa14259 Binary files /dev/null and b/assets/SLA_Assets/Ammo/5mm-smg__HEAP.png differ diff --git a/assets/SLA_Assets/Ammo/5mm-smg__STD.png b/assets/SLA_Assets/Ammo/5mm-smg__STD.png new file mode 100644 index 0000000..ca3fafc Binary files /dev/null and b/assets/SLA_Assets/Ammo/5mm-smg__STD.png differ diff --git a/assets/SLA_Assets/Ammo/5mm__AP.png b/assets/SLA_Assets/Ammo/5mm__AP.png new file mode 100644 index 0000000..2bac9a4 Binary files /dev/null and b/assets/SLA_Assets/Ammo/5mm__AP.png differ diff --git a/assets/SLA_Assets/Ammo/5mm__HE.png b/assets/SLA_Assets/Ammo/5mm__HE.png new file mode 100644 index 0000000..c22999a Binary files /dev/null and b/assets/SLA_Assets/Ammo/5mm__HE.png differ diff --git a/assets/SLA_Assets/Ammo/5mm__HEAP.png b/assets/SLA_Assets/Ammo/5mm__HEAP.png new file mode 100644 index 0000000..19bc292 Binary files /dev/null and b/assets/SLA_Assets/Ammo/5mm__HEAP.png differ diff --git a/assets/SLA_Assets/Ammo/5mm__STD.png b/assets/SLA_Assets/Ammo/5mm__STD.png new file mode 100644 index 0000000..1caca53 Binary files /dev/null and b/assets/SLA_Assets/Ammo/5mm__STD.png differ diff --git a/assets/SLA_Assets/Ammo/7mm-rifle__AP.png b/assets/SLA_Assets/Ammo/7mm-rifle__AP.png new file mode 100644 index 0000000..5a8972a Binary files /dev/null and b/assets/SLA_Assets/Ammo/7mm-rifle__AP.png differ diff --git a/assets/SLA_Assets/Ammo/7mm-rifle__HE.png b/assets/SLA_Assets/Ammo/7mm-rifle__HE.png new file mode 100644 index 0000000..6dc7049 Binary files /dev/null and b/assets/SLA_Assets/Ammo/7mm-rifle__HE.png differ diff --git a/assets/SLA_Assets/Ammo/7mm-rifle__HEAP.png b/assets/SLA_Assets/Ammo/7mm-rifle__HEAP.png new file mode 100644 index 0000000..f2270d8 Binary files /dev/null and b/assets/SLA_Assets/Ammo/7mm-rifle__HEAP.png differ diff --git a/assets/SLA_Assets/Ammo/7mm-rifle__STD.png b/assets/SLA_Assets/Ammo/7mm-rifle__STD.png new file mode 100644 index 0000000..ab95522 Binary files /dev/null and b/assets/SLA_Assets/Ammo/7mm-rifle__STD.png differ diff --git a/assets/SLA_Assets/Ammo/8mm-rifle__AP.png b/assets/SLA_Assets/Ammo/8mm-rifle__AP.png new file mode 100644 index 0000000..915b673 Binary files /dev/null and b/assets/SLA_Assets/Ammo/8mm-rifle__AP.png differ diff --git a/assets/SLA_Assets/Ammo/8mm-rifle__HE.png b/assets/SLA_Assets/Ammo/8mm-rifle__HE.png new file mode 100644 index 0000000..f6559bd Binary files /dev/null and b/assets/SLA_Assets/Ammo/8mm-rifle__HE.png differ diff --git a/assets/SLA_Assets/Ammo/8mm-rifle__HEAP.png b/assets/SLA_Assets/Ammo/8mm-rifle__HEAP.png new file mode 100644 index 0000000..e566ef9 Binary files /dev/null and b/assets/SLA_Assets/Ammo/8mm-rifle__HEAP.png differ diff --git a/assets/SLA_Assets/Ammo/8mm-rifle__STD.png b/assets/SLA_Assets/Ammo/8mm-rifle__STD.png new file mode 100644 index 0000000..36bc54d Binary files /dev/null and b/assets/SLA_Assets/Ammo/8mm-rifle__STD.png differ diff --git a/assets/SLA_Assets/Ammo/8mm__AP.png b/assets/SLA_Assets/Ammo/8mm__AP.png new file mode 100644 index 0000000..56cb11c Binary files /dev/null and b/assets/SLA_Assets/Ammo/8mm__AP.png differ diff --git a/assets/SLA_Assets/Ammo/8mm__HE.png b/assets/SLA_Assets/Ammo/8mm__HE.png new file mode 100644 index 0000000..9951f18 Binary files /dev/null and b/assets/SLA_Assets/Ammo/8mm__HE.png differ diff --git a/assets/SLA_Assets/Ammo/8mm__HEAP.png b/assets/SLA_Assets/Ammo/8mm__HEAP.png new file mode 100644 index 0000000..95995d2 Binary files /dev/null and b/assets/SLA_Assets/Ammo/8mm__HEAP.png differ diff --git a/assets/SLA_Assets/Ammo/8mm__STD.png b/assets/SLA_Assets/Ammo/8mm__STD.png new file mode 100644 index 0000000..6bd8c11 Binary files /dev/null and b/assets/SLA_Assets/Ammo/8mm__STD.png differ diff --git a/assets/SLA_Assets/Armor/2nd try/CAF.png b/assets/SLA_Assets/Armor/2nd try/CAF.png new file mode 100644 index 0000000..c1f3422 Binary files /dev/null and b/assets/SLA_Assets/Armor/2nd try/CAF.png differ diff --git a/assets/SLA_Assets/Armor/2nd try/Chippy.png b/assets/SLA_Assets/Armor/2nd try/Chippy.png new file mode 100644 index 0000000..81840f3 Binary files /dev/null and b/assets/SLA_Assets/Armor/2nd try/Chippy.png differ diff --git a/assets/SLA_Assets/Armor/2nd try/Crackshot.png b/assets/SLA_Assets/Armor/2nd try/Crackshot.png new file mode 100644 index 0000000..9820028 Binary files /dev/null and b/assets/SLA_Assets/Armor/2nd try/Crackshot.png differ diff --git a/assets/SLA_Assets/Armor/2nd try/Dark Night Power.png b/assets/SLA_Assets/Armor/2nd try/Dark Night Power.png new file mode 100644 index 0000000..dc0cbd5 Binary files /dev/null and b/assets/SLA_Assets/Armor/2nd try/Dark Night Power.png differ diff --git a/assets/SLA_Assets/Armor/2nd try/Dodgybone.png b/assets/SLA_Assets/Armor/2nd try/Dodgybone.png new file mode 100644 index 0000000..e085d56 Binary files /dev/null and b/assets/SLA_Assets/Armor/2nd try/Dodgybone.png differ diff --git a/assets/SLA_Assets/Armor/2nd try/Ebb Dethsuit.png b/assets/SLA_Assets/Armor/2nd try/Ebb Dethsuit.png new file mode 100644 index 0000000..d9e00b6 Binary files /dev/null and b/assets/SLA_Assets/Armor/2nd try/Ebb Dethsuit.png differ diff --git a/assets/SLA_Assets/Armor/2nd try/Hazmat.png b/assets/SLA_Assets/Armor/2nd try/Hazmat.png new file mode 100644 index 0000000..efe2671 Binary files /dev/null and b/assets/SLA_Assets/Armor/2nd try/Hazmat.png differ diff --git a/assets/SLA_Assets/Armor/2nd try/Heavy Thresher.png b/assets/SLA_Assets/Armor/2nd try/Heavy Thresher.png new file mode 100644 index 0000000..c79d427 Binary files /dev/null and b/assets/SLA_Assets/Armor/2nd try/Heavy Thresher.png differ diff --git a/assets/SLA_Assets/Armor/2nd try/Light Thresher.png b/assets/SLA_Assets/Armor/2nd try/Light Thresher.png new file mode 100644 index 0000000..61b474a Binary files /dev/null and b/assets/SLA_Assets/Armor/2nd try/Light Thresher.png differ diff --git a/assets/SLA_Assets/Armor/2nd try/PP644 Body Blocker.png b/assets/SLA_Assets/Armor/2nd try/PP644 Body Blocker.png new file mode 100644 index 0000000..2d7d665 Binary files /dev/null and b/assets/SLA_Assets/Armor/2nd try/PP644 Body Blocker.png differ diff --git a/assets/SLA_Assets/Armor/2nd try/Riot Shield.png b/assets/SLA_Assets/Armor/2nd try/Riot Shield.png new file mode 100644 index 0000000..465f5f7 Binary files /dev/null and b/assets/SLA_Assets/Armor/2nd try/Riot Shield.png differ diff --git a/assets/SLA_Assets/Armor/2nd try/SLA Combat Armor.png b/assets/SLA_Assets/Armor/2nd try/SLA Combat Armor.png new file mode 100644 index 0000000..151d457 Binary files /dev/null and b/assets/SLA_Assets/Armor/2nd try/SLA Combat Armor.png differ diff --git a/assets/SLA_Assets/Armor/2nd try/SLA Covert Vest.png b/assets/SLA_Assets/Armor/2nd try/SLA Covert Vest.png new file mode 100644 index 0000000..ed4007e Binary files /dev/null and b/assets/SLA_Assets/Armor/2nd try/SLA Covert Vest.png differ diff --git a/assets/SLA_Assets/Armor/2nd try/SLA Heavy Assult.png b/assets/SLA_Assets/Armor/2nd try/SLA Heavy Assult.png new file mode 100644 index 0000000..0051829 Binary files /dev/null and b/assets/SLA_Assets/Armor/2nd try/SLA Heavy Assult.png differ diff --git a/assets/SLA_Assets/Armor/2nd try/SLA Patrole Armour.png b/assets/SLA_Assets/Armor/2nd try/SLA Patrole Armour.png new file mode 100644 index 0000000..17374fd Binary files /dev/null and b/assets/SLA_Assets/Armor/2nd try/SLA Patrole Armour.png differ diff --git a/assets/SLA_Assets/Armor/2nd try/SLA Recon Mesh.png b/assets/SLA_Assets/Armor/2nd try/SLA Recon Mesh.png new file mode 100644 index 0000000..bc54dd8 Binary files /dev/null and b/assets/SLA_Assets/Armor/2nd try/SLA Recon Mesh.png differ diff --git a/assets/SLA_Assets/Armor/2nd try/Shiver Flak Vest.png b/assets/SLA_Assets/Armor/2nd try/Shiver Flak Vest.png new file mode 100644 index 0000000..b82d6a0 Binary files /dev/null and b/assets/SLA_Assets/Armor/2nd try/Shiver Flak Vest.png differ diff --git a/assets/SLA_Assets/Armor/2nd try/Shiver.png b/assets/SLA_Assets/Armor/2nd try/Shiver.png new file mode 100644 index 0000000..c692810 Binary files /dev/null and b/assets/SLA_Assets/Armor/2nd try/Shiver.png differ diff --git a/assets/SLA_Assets/Armor/2nd try/Shock Armor.png b/assets/SLA_Assets/Armor/2nd try/Shock Armor.png new file mode 100644 index 0000000..212fe19 Binary files /dev/null and b/assets/SLA_Assets/Armor/2nd try/Shock Armor.png differ diff --git a/assets/SLA_Assets/Armor/2nd try/Stealth Suit.png b/assets/SLA_Assets/Armor/2nd try/Stealth Suit.png new file mode 100644 index 0000000..347cf0c Binary files /dev/null and b/assets/SLA_Assets/Armor/2nd try/Stealth Suit.png differ diff --git a/assets/SLA_Assets/Armor/2nd try/Street Cloths.png b/assets/SLA_Assets/Armor/2nd try/Street Cloths.png new file mode 100644 index 0000000..35ee14c Binary files /dev/null and b/assets/SLA_Assets/Armor/2nd try/Street Cloths.png differ diff --git a/assets/SLA_Assets/Armor/Body Block.png b/assets/SLA_Assets/Armor/Body Block.png new file mode 100644 index 0000000..ab79d11 Binary files /dev/null and b/assets/SLA_Assets/Armor/Body Block.png differ diff --git a/assets/SLA_Assets/Armor/CAF Armour.png b/assets/SLA_Assets/Armor/CAF Armour.png new file mode 100644 index 0000000..e017a34 Binary files /dev/null and b/assets/SLA_Assets/Armor/CAF Armour.png differ diff --git a/assets/SLA_Assets/Armor/DN Power A.png b/assets/SLA_Assets/Armor/DN Power A.png new file mode 100644 index 0000000..12e686a Binary files /dev/null and b/assets/SLA_Assets/Armor/DN Power A.png differ diff --git a/assets/SLA_Assets/Armor/Derath Suit.png b/assets/SLA_Assets/Armor/Derath Suit.png new file mode 100644 index 0000000..3376890 Binary files /dev/null and b/assets/SLA_Assets/Armor/Derath Suit.png differ diff --git a/assets/SLA_Assets/Armor/Downtown Jacket.png b/assets/SLA_Assets/Armor/Downtown Jacket.png new file mode 100644 index 0000000..7f38ef6 Binary files /dev/null and b/assets/SLA_Assets/Armor/Downtown Jacket.png differ diff --git a/assets/SLA_Assets/Armor/Exo Armor.png b/assets/SLA_Assets/Armor/Exo Armor.png new file mode 100644 index 0000000..fb86703 Binary files /dev/null and b/assets/SLA_Assets/Armor/Exo Armor.png differ diff --git a/assets/SLA_Assets/Armor/Hard Armor.png b/assets/SLA_Assets/Armor/Hard Armor.png new file mode 100644 index 0000000..921b9cb Binary files /dev/null and b/assets/SLA_Assets/Armor/Hard Armor.png differ diff --git a/assets/SLA_Assets/Armor/Scout Armor.png b/assets/SLA_Assets/Armor/Scout Armor.png new file mode 100644 index 0000000..ad89dfa Binary files /dev/null and b/assets/SLA_Assets/Armor/Scout Armor.png differ diff --git a/assets/SLA_Assets/Armor/Shock A.png b/assets/SLA_Assets/Armor/Shock A.png new file mode 100644 index 0000000..a80042f Binary files /dev/null and b/assets/SLA_Assets/Armor/Shock A.png differ diff --git a/assets/SLA_Assets/Armor/Squad Shieled Power Armor.png b/assets/SLA_Assets/Armor/Squad Shieled Power Armor.png new file mode 100644 index 0000000..78e7f7e Binary files /dev/null and b/assets/SLA_Assets/Armor/Squad Shieled Power Armor.png differ diff --git a/assets/SLA_Assets/Drugs/2nd try/Blaze UV.png b/assets/SLA_Assets/Drugs/2nd try/Blaze UV.png new file mode 100644 index 0000000..ed524e3 Binary files /dev/null and b/assets/SLA_Assets/Drugs/2nd try/Blaze UV.png differ diff --git a/assets/SLA_Assets/Drugs/2nd try/Bozerker.png b/assets/SLA_Assets/Drugs/2nd try/Bozerker.png new file mode 100644 index 0000000..9958d9c Binary files /dev/null and b/assets/SLA_Assets/Drugs/2nd try/Bozerker.png differ diff --git a/assets/SLA_Assets/Drugs/2nd try/Bubbles.png b/assets/SLA_Assets/Drugs/2nd try/Bubbles.png new file mode 100644 index 0000000..15f4380 Binary files /dev/null and b/assets/SLA_Assets/Drugs/2nd try/Bubbles.png differ diff --git a/assets/SLA_Assets/Drugs/2nd try/Drum.png b/assets/SLA_Assets/Drugs/2nd try/Drum.png new file mode 100644 index 0000000..e7d23be Binary files /dev/null and b/assets/SLA_Assets/Drugs/2nd try/Drum.png differ diff --git a/assets/SLA_Assets/Drugs/2nd try/Flip.png b/assets/SLA_Assets/Drugs/2nd try/Flip.png new file mode 100644 index 0000000..c401c31 Binary files /dev/null and b/assets/SLA_Assets/Drugs/2nd try/Flip.png differ diff --git a/assets/SLA_Assets/Drugs/2nd try/Honesty.png b/assets/SLA_Assets/Drugs/2nd try/Honesty.png new file mode 100644 index 0000000..a5e825d Binary files /dev/null and b/assets/SLA_Assets/Drugs/2nd try/Honesty.png differ diff --git a/assets/SLA_Assets/Drugs/2nd try/Push.png b/assets/SLA_Assets/Drugs/2nd try/Push.png new file mode 100644 index 0000000..5b6175c Binary files /dev/null and b/assets/SLA_Assets/Drugs/2nd try/Push.png differ diff --git a/assets/SLA_Assets/Drugs/2nd try/Rush.png b/assets/SLA_Assets/Drugs/2nd try/Rush.png new file mode 100644 index 0000000..fd90a01 Binary files /dev/null and b/assets/SLA_Assets/Drugs/2nd try/Rush.png differ diff --git a/assets/SLA_Assets/Drugs/2nd try/Slosh.png b/assets/SLA_Assets/Drugs/2nd try/Slosh.png new file mode 100644 index 0000000..c3367f1 Binary files /dev/null and b/assets/SLA_Assets/Drugs/2nd try/Slosh.png differ diff --git a/assets/SLA_Assets/Drugs/2nd try/shatter.png b/assets/SLA_Assets/Drugs/2nd try/shatter.png new file mode 100644 index 0000000..3c2770e Binary files /dev/null and b/assets/SLA_Assets/Drugs/2nd try/shatter.png differ diff --git a/assets/SLA_Assets/Drugs/Calmline.png b/assets/SLA_Assets/Drugs/Calmline.png new file mode 100644 index 0000000..c3b6efd Binary files /dev/null and b/assets/SLA_Assets/Drugs/Calmline.png differ diff --git a/assets/SLA_Assets/Drugs/Generated/blaze.svg b/assets/SLA_Assets/Drugs/Generated/blaze.svg new file mode 100644 index 0000000..cee6d93 --- /dev/null +++ b/assets/SLA_Assets/Drugs/Generated/blaze.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + B + + BLAZE UV + COMBAT + \ No newline at end of file diff --git a/assets/SLA_Assets/Drugs/Generated/bozerker.svg b/assets/SLA_Assets/Drugs/Generated/bozerker.svg new file mode 100644 index 0000000..0a6f483 --- /dev/null +++ b/assets/SLA_Assets/Drugs/Generated/bozerker.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + Z + + BOZERKER + COMBAT + \ No newline at end of file diff --git a/assets/SLA_Assets/Drugs/Generated/bubbles.svg b/assets/SLA_Assets/Drugs/Generated/bubbles.svg new file mode 100644 index 0000000..faa4649 --- /dev/null +++ b/assets/SLA_Assets/Drugs/Generated/bubbles.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + U + + BUBBLES + SOFT + \ No newline at end of file diff --git a/assets/SLA_Assets/Drugs/Generated/drum.svg b/assets/SLA_Assets/Drugs/Generated/drum.svg new file mode 100644 index 0000000..51857e9 --- /dev/null +++ b/assets/SLA_Assets/Drugs/Generated/drum.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + D + + DRUM + COMBAT + \ No newline at end of file diff --git a/assets/SLA_Assets/Drugs/Generated/flip.svg b/assets/SLA_Assets/Drugs/Generated/flip.svg new file mode 100644 index 0000000..8c22936 --- /dev/null +++ b/assets/SLA_Assets/Drugs/Generated/flip.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + F + + FLIP + SOCIAL + \ No newline at end of file diff --git a/assets/SLA_Assets/Drugs/Generated/honesty.svg b/assets/SLA_Assets/Drugs/Generated/honesty.svg new file mode 100644 index 0000000..f9337cc --- /dev/null +++ b/assets/SLA_Assets/Drugs/Generated/honesty.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + H + + HONESTY + PSYCH + \ No newline at end of file diff --git a/assets/SLA_Assets/Drugs/Generated/kickstart-plus.svg b/assets/SLA_Assets/Drugs/Generated/kickstart-plus.svg new file mode 100644 index 0000000..e9d696c --- /dev/null +++ b/assets/SLA_Assets/Drugs/Generated/kickstart-plus.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + KICKSTART PLUS + ENHANCED + diff --git a/assets/SLA_Assets/Drugs/Generated/kickstart-solo.svg b/assets/SLA_Assets/Drugs/Generated/kickstart-solo.svg new file mode 100644 index 0000000..13308f2 --- /dev/null +++ b/assets/SLA_Assets/Drugs/Generated/kickstart-solo.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + KICKSTART SOLO + AUTO-INJECT + diff --git a/assets/SLA_Assets/Drugs/Generated/kickstart.svg b/assets/SLA_Assets/Drugs/Generated/kickstart.svg new file mode 100644 index 0000000..5589c66 --- /dev/null +++ b/assets/SLA_Assets/Drugs/Generated/kickstart.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + KICKSTART + MEDICAL + diff --git a/assets/SLA_Assets/Drugs/Generated/push.svg b/assets/SLA_Assets/Drugs/Generated/push.svg new file mode 100644 index 0000000..105eaa6 --- /dev/null +++ b/assets/SLA_Assets/Drugs/Generated/push.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + P + + PUSH + MEDICAL + \ No newline at end of file diff --git a/assets/SLA_Assets/Drugs/Generated/rush.svg b/assets/SLA_Assets/Drugs/Generated/rush.svg new file mode 100644 index 0000000..89b35a6 --- /dev/null +++ b/assets/SLA_Assets/Drugs/Generated/rush.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + R + + RUSH + COMBAT + \ No newline at end of file diff --git a/assets/SLA_Assets/Drugs/Generated/shatter.svg b/assets/SLA_Assets/Drugs/Generated/shatter.svg new file mode 100644 index 0000000..624178f --- /dev/null +++ b/assets/SLA_Assets/Drugs/Generated/shatter.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + S + + SHATTER + COMBAT + \ No newline at end of file diff --git a/assets/SLA_Assets/Drugs/Generated/slosh.svg b/assets/SLA_Assets/Drugs/Generated/slosh.svg new file mode 100644 index 0000000..be07f9b --- /dev/null +++ b/assets/SLA_Assets/Drugs/Generated/slosh.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + L + + SLOSH + SOFT + \ No newline at end of file diff --git a/assets/SLA_Assets/Drugs/Painkiller.png b/assets/SLA_Assets/Drugs/Painkiller.png new file mode 100644 index 0000000..1aa0b2e Binary files /dev/null and b/assets/SLA_Assets/Drugs/Painkiller.png differ diff --git a/assets/SLA_Assets/Drugs/Redline.png b/assets/SLA_Assets/Drugs/Redline.png new file mode 100644 index 0000000..c899998 Binary files /dev/null and b/assets/SLA_Assets/Drugs/Redline.png differ diff --git a/assets/SLA_Assets/Drugs/Rush.png b/assets/SLA_Assets/Drugs/Rush.png new file mode 100644 index 0000000..9979a4c Binary files /dev/null and b/assets/SLA_Assets/Drugs/Rush.png differ diff --git a/assets/SLA_Assets/Drugs/Sedative.png b/assets/SLA_Assets/Drugs/Sedative.png new file mode 100644 index 0000000..75b24c6 Binary files /dev/null and b/assets/SLA_Assets/Drugs/Sedative.png differ diff --git a/assets/SLA_Assets/Drugs/Stimulant.png b/assets/SLA_Assets/Drugs/Stimulant.png new file mode 100644 index 0000000..bf764cc Binary files /dev/null and b/assets/SLA_Assets/Drugs/Stimulant.png differ diff --git a/assets/SLA_Assets/Drugs/UV.png b/assets/SLA_Assets/Drugs/UV.png new file mode 100644 index 0000000..4f64d7c Binary files /dev/null and b/assets/SLA_Assets/Drugs/UV.png differ diff --git a/assets/SLA_Assets/Ebb/Awareness.png b/assets/SLA_Assets/Ebb/Awareness.png new file mode 100644 index 0000000..2ee2f11 Binary files /dev/null and b/assets/SLA_Assets/Ebb/Awareness.png differ diff --git a/assets/SLA_Assets/Ebb/Blast.png b/assets/SLA_Assets/Ebb/Blast.png new file mode 100644 index 0000000..1bdd369 Binary files /dev/null and b/assets/SLA_Assets/Ebb/Blast.png differ diff --git a/assets/SLA_Assets/Ebb/Communicate.png b/assets/SLA_Assets/Ebb/Communicate.png new file mode 100644 index 0000000..9da40c4 Binary files /dev/null and b/assets/SLA_Assets/Ebb/Communicate.png differ diff --git a/assets/SLA_Assets/Ebb/Ebb Heal.png b/assets/SLA_Assets/Ebb/Ebb Heal.png new file mode 100644 index 0000000..edee280 Binary files /dev/null and b/assets/SLA_Assets/Ebb/Ebb Heal.png differ diff --git a/assets/SLA_Assets/Ebb/Enhancement.png b/assets/SLA_Assets/Ebb/Enhancement.png new file mode 100644 index 0000000..7a83a47 Binary files /dev/null and b/assets/SLA_Assets/Ebb/Enhancement.png differ diff --git a/assets/SLA_Assets/Ebb/Gore Cannon.png b/assets/SLA_Assets/Ebb/Gore Cannon.png new file mode 100644 index 0000000..68568dc Binary files /dev/null and b/assets/SLA_Assets/Ebb/Gore Cannon.png differ diff --git a/assets/SLA_Assets/Ebb/Heal.png b/assets/SLA_Assets/Ebb/Heal.png new file mode 100644 index 0000000..ac548ac Binary files /dev/null and b/assets/SLA_Assets/Ebb/Heal.png differ diff --git a/assets/SLA_Assets/Ebb/Protect.png b/assets/SLA_Assets/Ebb/Protect.png new file mode 100644 index 0000000..7a6bee6 Binary files /dev/null and b/assets/SLA_Assets/Ebb/Protect.png differ diff --git a/assets/SLA_Assets/Ebb/Reality Fold.png b/assets/SLA_Assets/Ebb/Reality Fold.png new file mode 100644 index 0000000..d26da4d Binary files /dev/null and b/assets/SLA_Assets/Ebb/Reality Fold.png differ diff --git a/assets/SLA_Assets/Ebb/Senses.png b/assets/SLA_Assets/Ebb/Senses.png new file mode 100644 index 0000000..181b35b Binary files /dev/null and b/assets/SLA_Assets/Ebb/Senses.png differ diff --git a/assets/SLA_Assets/Ebb/Telekinesis.png b/assets/SLA_Assets/Ebb/Telekinesis.png new file mode 100644 index 0000000..003b18c Binary files /dev/null and b/assets/SLA_Assets/Ebb/Telekinesis.png differ diff --git a/assets/SLA_Assets/Ebb/Thermal Blue.png b/assets/SLA_Assets/Ebb/Thermal Blue.png new file mode 100644 index 0000000..9f71619 Binary files /dev/null and b/assets/SLA_Assets/Ebb/Thermal Blue.png differ diff --git a/assets/SLA_Assets/Ebb/Thermal Red.png b/assets/SLA_Assets/Ebb/Thermal Red.png new file mode 100644 index 0000000..b3df856 Binary files /dev/null and b/assets/SLA_Assets/Ebb/Thermal Red.png differ diff --git a/assets/SLA_Assets/Ebb/Training/Death Squad.png b/assets/SLA_Assets/Ebb/Training/Death Squad.png new file mode 100644 index 0000000..6266393 Binary files /dev/null and b/assets/SLA_Assets/Ebb/Training/Death Squad.png differ diff --git a/assets/SLA_Assets/Ebb/Training/EBB Operative.png b/assets/SLA_Assets/Ebb/Training/EBB Operative.png new file mode 100644 index 0000000..b1c7c48 Binary files /dev/null and b/assets/SLA_Assets/Ebb/Training/EBB Operative.png differ diff --git a/assets/SLA_Assets/Ebb/Training/INvestigation.png b/assets/SLA_Assets/Ebb/Training/INvestigation.png new file mode 100644 index 0000000..4b6092b Binary files /dev/null and b/assets/SLA_Assets/Ebb/Training/INvestigation.png differ diff --git a/assets/SLA_Assets/Ebb/Training/Kick Murder Squad.png b/assets/SLA_Assets/Ebb/Training/Kick Murder Squad.png new file mode 100644 index 0000000..8899e08 Binary files /dev/null and b/assets/SLA_Assets/Ebb/Training/Kick Murder Squad.png differ diff --git a/assets/SLA_Assets/Ebb/Training/Kick Murder.png b/assets/SLA_Assets/Ebb/Training/Kick Murder.png new file mode 100644 index 0000000..155bb9f Binary files /dev/null and b/assets/SLA_Assets/Ebb/Training/Kick Murder.png differ diff --git a/assets/SLA_Assets/Ebb/Training/Mechanic Tech Ops.png b/assets/SLA_Assets/Ebb/Training/Mechanic Tech Ops.png new file mode 100644 index 0000000..b9ce9a5 Binary files /dev/null and b/assets/SLA_Assets/Ebb/Training/Mechanic Tech Ops.png differ diff --git a/assets/SLA_Assets/Ebb/Training/Mechanic.png b/assets/SLA_Assets/Ebb/Training/Mechanic.png new file mode 100644 index 0000000..daf36e1 Binary files /dev/null and b/assets/SLA_Assets/Ebb/Training/Mechanic.png differ diff --git a/assets/SLA_Assets/Ebb/Training/Media.png b/assets/SLA_Assets/Ebb/Training/Media.png new file mode 100644 index 0000000..bdf4fb4 Binary files /dev/null and b/assets/SLA_Assets/Ebb/Training/Media.png differ diff --git a/assets/SLA_Assets/Ebb/Training/Medical.png b/assets/SLA_Assets/Ebb/Training/Medical.png new file mode 100644 index 0000000..3a4b12d Binary files /dev/null and b/assets/SLA_Assets/Ebb/Training/Medical.png differ diff --git a/assets/SLA_Assets/Ebb/Training/Pilot.png b/assets/SLA_Assets/Ebb/Training/Pilot.png new file mode 100644 index 0000000..64fdab3 Binary files /dev/null and b/assets/SLA_Assets/Ebb/Training/Pilot.png differ diff --git a/assets/SLA_Assets/Ebb/Training/Scouting.png b/assets/SLA_Assets/Ebb/Training/Scouting.png new file mode 100644 index 0000000..78ff6a1 Binary files /dev/null and b/assets/SLA_Assets/Ebb/Training/Scouting.png differ diff --git a/assets/SLA_Assets/Ebb/Training/Special Ops.png b/assets/SLA_Assets/Ebb/Training/Special Ops.png new file mode 100644 index 0000000..f9e4719 Binary files /dev/null and b/assets/SLA_Assets/Ebb/Training/Special Ops.png differ diff --git a/assets/SLA_Assets/Ebb/Training/Strike Squad.png b/assets/SLA_Assets/Ebb/Training/Strike Squad.png new file mode 100644 index 0000000..d8bda58 Binary files /dev/null and b/assets/SLA_Assets/Ebb/Training/Strike Squad.png differ diff --git a/assets/SLA_Assets/Ebb/Training/Tech Ops.png b/assets/SLA_Assets/Ebb/Training/Tech Ops.png new file mode 100644 index 0000000..d0673b0 Binary files /dev/null and b/assets/SLA_Assets/Ebb/Training/Tech Ops.png differ diff --git a/assets/SLA_Assets/Ebb/Training/media Elete.png b/assets/SLA_Assets/Ebb/Training/media Elete.png new file mode 100644 index 0000000..4334c74 Binary files /dev/null and b/assets/SLA_Assets/Ebb/Training/media Elete.png differ diff --git a/assets/SLA_Assets/Ebb/Training/pilot and Navigation.png b/assets/SLA_Assets/Ebb/Training/pilot and Navigation.png new file mode 100644 index 0000000..94f321d Binary files /dev/null and b/assets/SLA_Assets/Ebb/Training/pilot and Navigation.png differ diff --git a/assets/SLA_Assets/Gear/Ammo Box Sealed.png b/assets/SLA_Assets/Gear/Ammo Box Sealed.png new file mode 100644 index 0000000..47a6cc6 Binary files /dev/null and b/assets/SLA_Assets/Gear/Ammo Box Sealed.png differ diff --git a/assets/SLA_Assets/Gear/Audio recorder.png b/assets/SLA_Assets/Gear/Audio recorder.png new file mode 100644 index 0000000..373e119 Binary files /dev/null and b/assets/SLA_Assets/Gear/Audio recorder.png differ diff --git a/assets/SLA_Assets/Gear/Backpack.png b/assets/SLA_Assets/Gear/Backpack.png new file mode 100644 index 0000000..546bed2 Binary files /dev/null and b/assets/SLA_Assets/Gear/Backpack.png differ diff --git a/assets/SLA_Assets/Gear/Boopa Audio.png b/assets/SLA_Assets/Gear/Boopa Audio.png new file mode 100644 index 0000000..9e8408a Binary files /dev/null and b/assets/SLA_Assets/Gear/Boopa Audio.png differ diff --git a/assets/SLA_Assets/Gear/Boopa Medi Kit Advanced.png b/assets/SLA_Assets/Gear/Boopa Medi Kit Advanced.png new file mode 100644 index 0000000..a99b72f Binary files /dev/null and b/assets/SLA_Assets/Gear/Boopa Medi Kit Advanced.png differ diff --git a/assets/SLA_Assets/Gear/Breaching Frame Charge.png b/assets/SLA_Assets/Gear/Breaching Frame Charge.png new file mode 100644 index 0000000..b20b5bb Binary files /dev/null and b/assets/SLA_Assets/Gear/Breaching Frame Charge.png differ diff --git a/assets/SLA_Assets/Gear/Cable Ties.png b/assets/SLA_Assets/Gear/Cable Ties.png new file mode 100644 index 0000000..a35fd3c Binary files /dev/null and b/assets/SLA_Assets/Gear/Cable Ties.png differ diff --git a/assets/SLA_Assets/Gear/Chem Light Sticks.png b/assets/SLA_Assets/Gear/Chem Light Sticks.png new file mode 100644 index 0000000..2bf77ca Binary files /dev/null and b/assets/SLA_Assets/Gear/Chem Light Sticks.png differ diff --git a/assets/SLA_Assets/Gear/Climbing Harness.png b/assets/SLA_Assets/Gear/Climbing Harness.png new file mode 100644 index 0000000..92a58e0 Binary files /dev/null and b/assets/SLA_Assets/Gear/Climbing Harness.png differ diff --git a/assets/SLA_Assets/Gear/Compass Magnetic.png b/assets/SLA_Assets/Gear/Compass Magnetic.png new file mode 100644 index 0000000..d340003 Binary files /dev/null and b/assets/SLA_Assets/Gear/Compass Magnetic.png differ diff --git a/assets/SLA_Assets/Gear/Csnteen.png b/assets/SLA_Assets/Gear/Csnteen.png new file mode 100644 index 0000000..943b8ad Binary files /dev/null and b/assets/SLA_Assets/Gear/Csnteen.png differ diff --git a/assets/SLA_Assets/Gear/Cybernetic Maintenance Kit.png b/assets/SLA_Assets/Gear/Cybernetic Maintenance Kit.png new file mode 100644 index 0000000..5f9b79c Binary files /dev/null and b/assets/SLA_Assets/Gear/Cybernetic Maintenance Kit.png differ diff --git a/assets/SLA_Assets/Gear/Duct Tape Roll.png b/assets/SLA_Assets/Gear/Duct Tape Roll.png new file mode 100644 index 0000000..639da47 Binary files /dev/null and b/assets/SLA_Assets/Gear/Duct Tape Roll.png differ diff --git a/assets/SLA_Assets/Gear/Ebb Damper.png b/assets/SLA_Assets/Gear/Ebb Damper.png new file mode 100644 index 0000000..6dda8cb Binary files /dev/null and b/assets/SLA_Assets/Gear/Ebb Damper.png differ diff --git a/assets/SLA_Assets/Gear/Electric Lockpick.png b/assets/SLA_Assets/Gear/Electric Lockpick.png new file mode 100644 index 0000000..7d80dff Binary files /dev/null and b/assets/SLA_Assets/Gear/Electric Lockpick.png differ diff --git a/assets/SLA_Assets/Gear/Entrenching Tool.png b/assets/SLA_Assets/Gear/Entrenching Tool.png new file mode 100644 index 0000000..ce4ee48 Binary files /dev/null and b/assets/SLA_Assets/Gear/Entrenching Tool.png differ diff --git a/assets/SLA_Assets/Gear/Environment Scanner Standard.png b/assets/SLA_Assets/Gear/Environment Scanner Standard.png new file mode 100644 index 0000000..1919988 Binary files /dev/null and b/assets/SLA_Assets/Gear/Environment Scanner Standard.png differ diff --git a/assets/SLA_Assets/Gear/Environment Scanner.png b/assets/SLA_Assets/Gear/Environment Scanner.png new file mode 100644 index 0000000..0a567ce Binary files /dev/null and b/assets/SLA_Assets/Gear/Environment Scanner.png differ diff --git a/assets/SLA_Assets/Gear/Equipment Case Rugged.png b/assets/SLA_Assets/Gear/Equipment Case Rugged.png new file mode 100644 index 0000000..e6e7fc4 Binary files /dev/null and b/assets/SLA_Assets/Gear/Equipment Case Rugged.png differ diff --git a/assets/SLA_Assets/Gear/Evidence Bags.png b/assets/SLA_Assets/Gear/Evidence Bags.png new file mode 100644 index 0000000..cb1aec9 Binary files /dev/null and b/assets/SLA_Assets/Gear/Evidence Bags.png differ diff --git a/assets/SLA_Assets/Gear/Flares Pack of 6.png b/assets/SLA_Assets/Gear/Flares Pack of 6.png new file mode 100644 index 0000000..60194e4 Binary files /dev/null and b/assets/SLA_Assets/Gear/Flares Pack of 6.png differ diff --git a/assets/SLA_Assets/Gear/FlashBang .png b/assets/SLA_Assets/Gear/FlashBang .png new file mode 100644 index 0000000..3c1f244 Binary files /dev/null and b/assets/SLA_Assets/Gear/FlashBang .png differ diff --git a/assets/SLA_Assets/Gear/Forensic Kit.png b/assets/SLA_Assets/Gear/Forensic Kit.png new file mode 100644 index 0000000..c5867cd Binary files /dev/null and b/assets/SLA_Assets/Gear/Forensic Kit.png differ diff --git a/assets/SLA_Assets/Gear/Grapple Gun.png b/assets/SLA_Assets/Gear/Grapple Gun.png new file mode 100644 index 0000000..1bbb33e Binary files /dev/null and b/assets/SLA_Assets/Gear/Grapple Gun.png differ diff --git a/assets/SLA_Assets/Gear/Grooming Kit.png b/assets/SLA_Assets/Gear/Grooming Kit.png new file mode 100644 index 0000000..fff3da2 Binary files /dev/null and b/assets/SLA_Assets/Gear/Grooming Kit.png differ diff --git a/assets/SLA_Assets/Gear/Headset Comms.png b/assets/SLA_Assets/Gear/Headset Comms.png new file mode 100644 index 0000000..174ba99 Binary files /dev/null and b/assets/SLA_Assets/Gear/Headset Comms.png differ diff --git a/assets/SLA_Assets/Gear/Holo Projector.png b/assets/SLA_Assets/Gear/Holo Projector.png new file mode 100644 index 0000000..ddece98 Binary files /dev/null and b/assets/SLA_Assets/Gear/Holo Projector.png differ diff --git a/assets/SLA_Assets/Gear/Interface Cabler.png b/assets/SLA_Assets/Gear/Interface Cabler.png new file mode 100644 index 0000000..3defaa2 Binary files /dev/null and b/assets/SLA_Assets/Gear/Interface Cabler.png differ diff --git a/assets/SLA_Assets/Gear/Klippo MultiBand Comm.png b/assets/SLA_Assets/Gear/Klippo MultiBand Comm.png new file mode 100644 index 0000000..c250862 Binary files /dev/null and b/assets/SLA_Assets/Gear/Klippo MultiBand Comm.png differ diff --git a/assets/SLA_Assets/Gear/Klippo Spray Hypo.png b/assets/SLA_Assets/Gear/Klippo Spray Hypo.png new file mode 100644 index 0000000..c238df5 Binary files /dev/null and b/assets/SLA_Assets/Gear/Klippo Spray Hypo.png differ diff --git a/assets/SLA_Assets/Gear/Lockpick Set Professional.png b/assets/SLA_Assets/Gear/Lockpick Set Professional.png new file mode 100644 index 0000000..9a5d4f8 Binary files /dev/null and b/assets/SLA_Assets/Gear/Lockpick Set Professional.png differ diff --git a/assets/SLA_Assets/Gear/MAC Knife.png b/assets/SLA_Assets/Gear/MAC Knife.png new file mode 100644 index 0000000..1a84df8 Binary files /dev/null and b/assets/SLA_Assets/Gear/MAC Knife.png differ diff --git a/assets/SLA_Assets/Gear/MRE.png b/assets/SLA_Assets/Gear/MRE.png new file mode 100644 index 0000000..efa4a7d Binary files /dev/null and b/assets/SLA_Assets/Gear/MRE.png differ diff --git a/assets/SLA_Assets/Gear/Maghold Boots.png b/assets/SLA_Assets/Gear/Maghold Boots.png new file mode 100644 index 0000000..a4a5335 Binary files /dev/null and b/assets/SLA_Assets/Gear/Maghold Boots.png differ diff --git a/assets/SLA_Assets/Gear/Maghold Gloves.png b/assets/SLA_Assets/Gear/Maghold Gloves.png new file mode 100644 index 0000000..b9a962c Binary files /dev/null and b/assets/SLA_Assets/Gear/Maghold Gloves.png differ diff --git a/assets/SLA_Assets/Gear/Medikit Disposable.png b/assets/SLA_Assets/Gear/Medikit Disposable.png new file mode 100644 index 0000000..1c9f8b9 Binary files /dev/null and b/assets/SLA_Assets/Gear/Medikit Disposable.png differ diff --git a/assets/SLA_Assets/Gear/Mirros Signal.png b/assets/SLA_Assets/Gear/Mirros Signal.png new file mode 100644 index 0000000..1b37360 Binary files /dev/null and b/assets/SLA_Assets/Gear/Mirros Signal.png differ diff --git a/assets/SLA_Assets/Gear/Motion Tracker.png b/assets/SLA_Assets/Gear/Motion Tracker.png new file mode 100644 index 0000000..de015ed Binary files /dev/null and b/assets/SLA_Assets/Gear/Motion Tracker.png differ diff --git a/assets/SLA_Assets/Gear/Oyster Card.png b/assets/SLA_Assets/Gear/Oyster Card.png new file mode 100644 index 0000000..d667ea1 Binary files /dev/null and b/assets/SLA_Assets/Gear/Oyster Card.png differ diff --git a/assets/SLA_Assets/Gear/Padlock.png b/assets/SLA_Assets/Gear/Padlock.png new file mode 100644 index 0000000..883f421 Binary files /dev/null and b/assets/SLA_Assets/Gear/Padlock.png differ diff --git a/assets/SLA_Assets/Gear/Playng Cards.png b/assets/SLA_Assets/Gear/Playng Cards.png new file mode 100644 index 0000000..f32060c Binary files /dev/null and b/assets/SLA_Assets/Gear/Playng Cards.png differ diff --git a/assets/SLA_Assets/Gear/Portable Genorator.png b/assets/SLA_Assets/Gear/Portable Genorator.png new file mode 100644 index 0000000..4523be4 Binary files /dev/null and b/assets/SLA_Assets/Gear/Portable Genorator.png differ diff --git a/assets/SLA_Assets/Gear/Power Cell Extended.png b/assets/SLA_Assets/Gear/Power Cell Extended.png new file mode 100644 index 0000000..a4dd243 Binary files /dev/null and b/assets/SLA_Assets/Gear/Power Cell Extended.png differ diff --git a/assets/SLA_Assets/Gear/Power Coupler.png b/assets/SLA_Assets/Gear/Power Coupler.png new file mode 100644 index 0000000..4cbd4e6 Binary files /dev/null and b/assets/SLA_Assets/Gear/Power Coupler.png differ diff --git a/assets/SLA_Assets/Gear/Power SCell Standard.png b/assets/SLA_Assets/Gear/Power SCell Standard.png new file mode 100644 index 0000000..e6795c2 Binary files /dev/null and b/assets/SLA_Assets/Gear/Power SCell Standard.png differ diff --git a/assets/SLA_Assets/Gear/Recharge Station Portable.png b/assets/SLA_Assets/Gear/Recharge Station Portable.png new file mode 100644 index 0000000..58e3299 Binary files /dev/null and b/assets/SLA_Assets/Gear/Recharge Station Portable.png differ diff --git a/assets/SLA_Assets/Gear/Restraint Cuffs.png b/assets/SLA_Assets/Gear/Restraint Cuffs.png new file mode 100644 index 0000000..cb45a9b Binary files /dev/null and b/assets/SLA_Assets/Gear/Restraint Cuffs.png differ diff --git a/assets/SLA_Assets/Gear/Rope.png b/assets/SLA_Assets/Gear/Rope.png new file mode 100644 index 0000000..e47bf08 Binary files /dev/null and b/assets/SLA_Assets/Gear/Rope.png differ diff --git a/assets/SLA_Assets/Gear/Sleeping Bag.png b/assets/SLA_Assets/Gear/Sleeping Bag.png new file mode 100644 index 0000000..6dc8924 Binary files /dev/null and b/assets/SLA_Assets/Gear/Sleeping Bag.png differ diff --git a/assets/SLA_Assets/Gear/Smoke Grenade.png b/assets/SLA_Assets/Gear/Smoke Grenade.png new file mode 100644 index 0000000..bcf0092 Binary files /dev/null and b/assets/SLA_Assets/Gear/Smoke Grenade.png differ diff --git a/assets/SLA_Assets/Gear/Standard Binoculars.png b/assets/SLA_Assets/Gear/Standard Binoculars.png new file mode 100644 index 0000000..99a7893 Binary files /dev/null and b/assets/SLA_Assets/Gear/Standard Binoculars.png differ diff --git a/assets/SLA_Assets/Gear/Thermal Blanket.png b/assets/SLA_Assets/Gear/Thermal Blanket.png new file mode 100644 index 0000000..15c36fd Binary files /dev/null and b/assets/SLA_Assets/Gear/Thermal Blanket.png differ diff --git a/assets/SLA_Assets/Gear/Tool Kit Electronic.png b/assets/SLA_Assets/Gear/Tool Kit Electronic.png new file mode 100644 index 0000000..702c970 Binary files /dev/null and b/assets/SLA_Assets/Gear/Tool Kit Electronic.png differ diff --git a/assets/SLA_Assets/Gear/Tool Kit General.png b/assets/SLA_Assets/Gear/Tool Kit General.png new file mode 100644 index 0000000..b140c93 Binary files /dev/null and b/assets/SLA_Assets/Gear/Tool Kit General.png differ diff --git a/assets/SLA_Assets/Gear/Trauma Stabiliser.png b/assets/SLA_Assets/Gear/Trauma Stabiliser.png new file mode 100644 index 0000000..ce06264 Binary files /dev/null and b/assets/SLA_Assets/Gear/Trauma Stabiliser.png differ diff --git a/assets/SLA_Assets/Gear/UV Multi Spectrum Torch.png b/assets/SLA_Assets/Gear/UV Multi Spectrum Torch.png new file mode 100644 index 0000000..3c3f41d Binary files /dev/null and b/assets/SLA_Assets/Gear/UV Multi Spectrum Torch.png differ diff --git a/assets/SLA_Assets/Gear/Vehicle Repair Kit.png b/assets/SLA_Assets/Gear/Vehicle Repair Kit.png new file mode 100644 index 0000000..0393fe3 Binary files /dev/null and b/assets/SLA_Assets/Gear/Vehicle Repair Kit.png differ diff --git a/assets/SLA_Assets/Gear/Vehicle Tool Kit.png b/assets/SLA_Assets/Gear/Vehicle Tool Kit.png new file mode 100644 index 0000000..d4c007f Binary files /dev/null and b/assets/SLA_Assets/Gear/Vehicle Tool Kit.png differ diff --git a/assets/SLA_Assets/Gear/Vid Phone Portable.png b/assets/SLA_Assets/Gear/Vid Phone Portable.png new file mode 100644 index 0000000..b51399f Binary files /dev/null and b/assets/SLA_Assets/Gear/Vid Phone Portable.png differ diff --git a/assets/SLA_Assets/Gear/Water Purify Tablets.png b/assets/SLA_Assets/Gear/Water Purify Tablets.png new file mode 100644 index 0000000..fb43f4e Binary files /dev/null and b/assets/SLA_Assets/Gear/Water Purify Tablets.png differ diff --git a/assets/SLA_Assets/Gear/Webbing Tacktical Vest.png b/assets/SLA_Assets/Gear/Webbing Tacktical Vest.png new file mode 100644 index 0000000..e50e48e Binary files /dev/null and b/assets/SLA_Assets/Gear/Webbing Tacktical Vest.png differ diff --git a/assets/SLA_Assets/Gear/binoculars Tacktical.png b/assets/SLA_Assets/Gear/binoculars Tacktical.png new file mode 100644 index 0000000..7252d8f Binary files /dev/null and b/assets/SLA_Assets/Gear/binoculars Tacktical.png differ diff --git a/assets/SLA_Assets/Gear/camera Still video.png b/assets/SLA_Assets/Gear/camera Still video.png new file mode 100644 index 0000000..3694205 Binary files /dev/null and b/assets/SLA_Assets/Gear/camera Still video.png differ diff --git a/assets/SLA_Assets/Gear/pprative organiser.png b/assets/SLA_Assets/Gear/pprative organiser.png new file mode 100644 index 0000000..04bead8 Binary files /dev/null and b/assets/SLA_Assets/Gear/pprative organiser.png differ diff --git a/assets/SLA_Assets/Maps/slaguru6666_The_words_SLA_INDUSTRIES_in_neon_80s_grafitti_on__bbbf7678-0361-4632-8a3f-4134e9a764dc_1.png b/assets/SLA_Assets/Maps/slaguru6666_The_words_SLA_INDUSTRIES_in_neon_80s_grafitti_on__bbbf7678-0361-4632-8a3f-4134e9a764dc_1.png new file mode 100644 index 0000000..0e15ea9 Binary files /dev/null and b/assets/SLA_Assets/Maps/slaguru6666_The_words_SLA_INDUSTRIES_in_neon_80s_grafitti_on__bbbf7678-0361-4632-8a3f-4134e9a764dc_1.png differ diff --git a/assets/SLA_Assets/PICS/2026-02-23_17-36-46.png b/assets/SLA_Assets/PICS/2026-02-23_17-36-46.png new file mode 100644 index 0000000..ff39559 Binary files /dev/null and b/assets/SLA_Assets/PICS/2026-02-23_17-36-46.png differ diff --git a/assets/SLA_Assets/PICS/2026-02-23_21-13-15.png b/assets/SLA_Assets/PICS/2026-02-23_21-13-15.png new file mode 100644 index 0000000..2cafceb Binary files /dev/null and b/assets/SLA_Assets/PICS/2026-02-23_21-13-15.png differ diff --git a/assets/SLA_Assets/PICS/2026-02-23_21-13-43.png b/assets/SLA_Assets/PICS/2026-02-23_21-13-43.png new file mode 100644 index 0000000..c1fb732 Binary files /dev/null and b/assets/SLA_Assets/PICS/2026-02-23_21-13-43.png differ diff --git a/assets/SLA_Assets/PICS/2026-02-23_21-35-55.png b/assets/SLA_Assets/PICS/2026-02-23_21-35-55.png new file mode 100644 index 0000000..1a11975 Binary files /dev/null and b/assets/SLA_Assets/PICS/2026-02-23_21-35-55.png differ diff --git a/assets/SLA_Assets/PICS/2026-02-23_21-51-21.png b/assets/SLA_Assets/PICS/2026-02-23_21-51-21.png new file mode 100644 index 0000000..a82de09 Binary files /dev/null and b/assets/SLA_Assets/PICS/2026-02-23_21-51-21.png differ diff --git a/assets/SLA_Assets/PICS/2026-02-23_21-51-36.png b/assets/SLA_Assets/PICS/2026-02-23_21-51-36.png new file mode 100644 index 0000000..2212ab8 Binary files /dev/null and b/assets/SLA_Assets/PICS/2026-02-23_21-51-36.png differ diff --git a/assets/SLA_Assets/PICS/2026-02-23_21-53-43.png b/assets/SLA_Assets/PICS/2026-02-23_21-53-43.png new file mode 100644 index 0000000..90f4d12 Binary files /dev/null and b/assets/SLA_Assets/PICS/2026-02-23_21-53-43.png differ diff --git a/assets/SLA_Assets/PICS/2026-02-23_21-54-20.png b/assets/SLA_Assets/PICS/2026-02-23_21-54-20.png new file mode 100644 index 0000000..5abf4ae Binary files /dev/null and b/assets/SLA_Assets/PICS/2026-02-23_21-54-20.png differ diff --git a/assets/SLA_Assets/PICS/2026-02-23_21-54-38.png b/assets/SLA_Assets/PICS/2026-02-23_21-54-38.png new file mode 100644 index 0000000..30eb8f4 Binary files /dev/null and b/assets/SLA_Assets/PICS/2026-02-23_21-54-38.png differ diff --git a/assets/SLA_Assets/PICS/2026-02-23_21-54-59.png b/assets/SLA_Assets/PICS/2026-02-23_21-54-59.png new file mode 100644 index 0000000..3d89362 Binary files /dev/null and b/assets/SLA_Assets/PICS/2026-02-23_21-54-59.png differ diff --git a/assets/SLA_Assets/PICS/2026-02-23_21-55-15.png b/assets/SLA_Assets/PICS/2026-02-23_21-55-15.png new file mode 100644 index 0000000..daa875b Binary files /dev/null and b/assets/SLA_Assets/PICS/2026-02-23_21-55-15.png differ diff --git a/assets/SLA_Assets/PICS/2026-02-23_21-55-31.png b/assets/SLA_Assets/PICS/2026-02-23_21-55-31.png new file mode 100644 index 0000000..5778dee Binary files /dev/null and b/assets/SLA_Assets/PICS/2026-02-23_21-55-31.png differ diff --git a/assets/SLA_Assets/PICS/2026-02-23_21-55-58.png b/assets/SLA_Assets/PICS/2026-02-23_21-55-58.png new file mode 100644 index 0000000..1c1e8ed Binary files /dev/null and b/assets/SLA_Assets/PICS/2026-02-23_21-55-58.png differ diff --git a/assets/SLA_Assets/PICS/2026-02-23_21-57-35.png b/assets/SLA_Assets/PICS/2026-02-23_21-57-35.png new file mode 100644 index 0000000..4c9a8d1 Binary files /dev/null and b/assets/SLA_Assets/PICS/2026-02-23_21-57-35.png differ diff --git a/assets/SLA_Assets/PICS/2026-02-23_21-58-15.png b/assets/SLA_Assets/PICS/2026-02-23_21-58-15.png new file mode 100644 index 0000000..56111f3 Binary files /dev/null and b/assets/SLA_Assets/PICS/2026-02-23_21-58-15.png differ diff --git a/assets/SLA_Assets/PICS/2026-02-23_21-58-28.png b/assets/SLA_Assets/PICS/2026-02-23_21-58-28.png new file mode 100644 index 0000000..28b1474 Binary files /dev/null and b/assets/SLA_Assets/PICS/2026-02-23_21-58-28.png differ diff --git a/assets/SLA_Assets/PICS/2026-02-23_21-58-45.png b/assets/SLA_Assets/PICS/2026-02-23_21-58-45.png new file mode 100644 index 0000000..b974d3a Binary files /dev/null and b/assets/SLA_Assets/PICS/2026-02-23_21-58-45.png differ diff --git a/assets/SLA_Assets/PICS/2026-02-23_22-13-23.png b/assets/SLA_Assets/PICS/2026-02-23_22-13-23.png new file mode 100644 index 0000000..f113ca5 Binary files /dev/null and b/assets/SLA_Assets/PICS/2026-02-23_22-13-23.png differ diff --git a/assets/SLA_Assets/PICS/2026-02-23_22-14-37.png b/assets/SLA_Assets/PICS/2026-02-23_22-14-37.png new file mode 100644 index 0000000..67cb142 Binary files /dev/null and b/assets/SLA_Assets/PICS/2026-02-23_22-14-37.png differ diff --git a/assets/SLA_Assets/PICS/Scenes Backgrounds/slaguru6666_grafitti_image_of_the_words_SLA_INDUSTRIES._It_sh_7d19a184-953c-47ed-8392-8f86a2f4e2a2_2.png b/assets/SLA_Assets/PICS/Scenes Backgrounds/slaguru6666_grafitti_image_of_the_words_SLA_INDUSTRIES._It_sh_7d19a184-953c-47ed-8392-8f86a2f4e2a2_2.png new file mode 100644 index 0000000..e12d420 Binary files /dev/null and b/assets/SLA_Assets/PICS/Scenes Backgrounds/slaguru6666_grafitti_image_of_the_words_SLA_INDUSTRIES._It_sh_7d19a184-953c-47ed-8392-8f86a2f4e2a2_2.png differ diff --git a/assets/SLA_Assets/People/OTTO.png b/assets/SLA_Assets/People/OTTO.png new file mode 100644 index 0000000..3e5e839 Binary files /dev/null and b/assets/SLA_Assets/People/OTTO.png differ diff --git a/assets/SLA_Assets/People/slaguru6666_34-year-old_tactical_detective_sharp_features_gra_a1892123-c066-41bc-8169-c6fa78eeaaed_3.png b/assets/SLA_Assets/People/slaguru6666_34-year-old_tactical_detective_sharp_features_gra_a1892123-c066-41bc-8169-c6fa78eeaaed_3.png new file mode 100644 index 0000000..454cdd7 Binary files /dev/null and b/assets/SLA_Assets/People/slaguru6666_34-year-old_tactical_detective_sharp_features_gra_a1892123-c066-41bc-8169-c6fa78eeaaed_3.png differ diff --git a/assets/SLA_Assets/Skills/Biofeedback.png b/assets/SLA_Assets/Skills/Biofeedback.png new file mode 100644 index 0000000..9393500 Binary files /dev/null and b/assets/SLA_Assets/Skills/Biofeedback.png differ diff --git a/assets/SLA_Assets/Skills/Bureaucracy.png b/assets/SLA_Assets/Skills/Bureaucracy.png new file mode 100644 index 0000000..580dfa3 Binary files /dev/null and b/assets/SLA_Assets/Skills/Bureaucracy.png differ diff --git a/assets/SLA_Assets/Skills/Climb.png b/assets/SLA_Assets/Skills/Climb.png new file mode 100644 index 0000000..2e70093 Binary files /dev/null and b/assets/SLA_Assets/Skills/Climb.png differ diff --git a/assets/SLA_Assets/Skills/Command.png b/assets/SLA_Assets/Skills/Command.png new file mode 100644 index 0000000..ab16af4 Binary files /dev/null and b/assets/SLA_Assets/Skills/Command.png differ diff --git a/assets/SLA_Assets/Skills/Computer Systems.png b/assets/SLA_Assets/Skills/Computer Systems.png new file mode 100644 index 0000000..7dd6182 Binary files /dev/null and b/assets/SLA_Assets/Skills/Computer Systems.png differ diff --git a/assets/SLA_Assets/Skills/Craft.png b/assets/SLA_Assets/Skills/Craft.png new file mode 100644 index 0000000..0943b5e Binary files /dev/null and b/assets/SLA_Assets/Skills/Craft.png differ diff --git a/assets/SLA_Assets/Skills/Dodge.png b/assets/SLA_Assets/Skills/Dodge.png new file mode 100644 index 0000000..83f573d Binary files /dev/null and b/assets/SLA_Assets/Skills/Dodge.png differ diff --git a/assets/SLA_Assets/Skills/Drive Civilian.png b/assets/SLA_Assets/Skills/Drive Civilian.png new file mode 100644 index 0000000..6e13c40 Binary files /dev/null and b/assets/SLA_Assets/Skills/Drive Civilian.png differ diff --git a/assets/SLA_Assets/Skills/Drive Military.png b/assets/SLA_Assets/Skills/Drive Military.png new file mode 100644 index 0000000..f7aa167 Binary files /dev/null and b/assets/SLA_Assets/Skills/Drive Military.png differ diff --git a/assets/SLA_Assets/Skills/EBB Awareness.png b/assets/SLA_Assets/Skills/EBB Awareness.png new file mode 100644 index 0000000..5681be6 Binary files /dev/null and b/assets/SLA_Assets/Skills/EBB Awareness.png differ diff --git a/assets/SLA_Assets/Skills/EBB Blast.png b/assets/SLA_Assets/Skills/EBB Blast.png new file mode 100644 index 0000000..0ad7a4c Binary files /dev/null and b/assets/SLA_Assets/Skills/EBB Blast.png differ diff --git a/assets/SLA_Assets/Skills/EBB Blue Thermal.png b/assets/SLA_Assets/Skills/EBB Blue Thermal.png new file mode 100644 index 0000000..7cd5d91 Binary files /dev/null and b/assets/SLA_Assets/Skills/EBB Blue Thermal.png differ diff --git a/assets/SLA_Assets/Skills/EBB Core.png b/assets/SLA_Assets/Skills/EBB Core.png new file mode 100644 index 0000000..4adbebd Binary files /dev/null and b/assets/SLA_Assets/Skills/EBB Core.png differ diff --git a/assets/SLA_Assets/Skills/EBB Reality Fold.png b/assets/SLA_Assets/Skills/EBB Reality Fold.png new file mode 100644 index 0000000..6b30236 Binary files /dev/null and b/assets/SLA_Assets/Skills/EBB Reality Fold.png differ diff --git a/assets/SLA_Assets/Skills/EBB Speciality.png b/assets/SLA_Assets/Skills/EBB Speciality.png new file mode 100644 index 0000000..7f3a761 Binary files /dev/null and b/assets/SLA_Assets/Skills/EBB Speciality.png differ diff --git a/assets/SLA_Assets/Skills/EBB Telekinesis.png b/assets/SLA_Assets/Skills/EBB Telekinesis.png new file mode 100644 index 0000000..09efa74 Binary files /dev/null and b/assets/SLA_Assets/Skills/EBB Telekinesis.png differ diff --git a/assets/SLA_Assets/Skills/EBB Thermal Red.png b/assets/SLA_Assets/Skills/EBB Thermal Red.png new file mode 100644 index 0000000..5830ecc Binary files /dev/null and b/assets/SLA_Assets/Skills/EBB Thermal Red.png differ diff --git a/assets/SLA_Assets/Skills/Ebb Communicate.png b/assets/SLA_Assets/Skills/Ebb Communicate.png new file mode 100644 index 0000000..56a3b26 Binary files /dev/null and b/assets/SLA_Assets/Skills/Ebb Communicate.png differ diff --git a/assets/SLA_Assets/Skills/Fast Talk.png b/assets/SLA_Assets/Skills/Fast Talk.png new file mode 100644 index 0000000..7407a26 Binary files /dev/null and b/assets/SLA_Assets/Skills/Fast Talk.png differ diff --git a/assets/SLA_Assets/Skills/Firearm Auto.png b/assets/SLA_Assets/Skills/Firearm Auto.png new file mode 100644 index 0000000..239bfba Binary files /dev/null and b/assets/SLA_Assets/Skills/Firearm Auto.png differ diff --git a/assets/SLA_Assets/Skills/Firearm Pistol.png b/assets/SLA_Assets/Skills/Firearm Pistol.png new file mode 100644 index 0000000..13772d7 Binary files /dev/null and b/assets/SLA_Assets/Skills/Firearm Pistol.png differ diff --git a/assets/SLA_Assets/Skills/Firearm Rifle:shotgun.png b/assets/SLA_Assets/Skills/Firearm Rifle:shotgun.png new file mode 100644 index 0000000..8eb0410 Binary files /dev/null and b/assets/SLA_Assets/Skills/Firearm Rifle:shotgun.png differ diff --git a/assets/SLA_Assets/Skills/Firearm SMG.png b/assets/SLA_Assets/Skills/Firearm SMG.png new file mode 100644 index 0000000..fffb42d Binary files /dev/null and b/assets/SLA_Assets/Skills/Firearm SMG.png differ diff --git a/assets/SLA_Assets/Skills/Formulate.png b/assets/SLA_Assets/Skills/Formulate.png new file mode 100644 index 0000000..d745ba0 Binary files /dev/null and b/assets/SLA_Assets/Skills/Formulate.png differ diff --git a/assets/SLA_Assets/Skills/Intimidate.png b/assets/SLA_Assets/Skills/Intimidate.png new file mode 100644 index 0000000..f7dc88c Binary files /dev/null and b/assets/SLA_Assets/Skills/Intimidate.png differ diff --git a/assets/SLA_Assets/Skills/Listen.png b/assets/SLA_Assets/Skills/Listen.png new file mode 100644 index 0000000..a380cb3 Binary files /dev/null and b/assets/SLA_Assets/Skills/Listen.png differ diff --git a/assets/SLA_Assets/Skills/Medical.png b/assets/SLA_Assets/Skills/Medical.png new file mode 100644 index 0000000..3e0e0f8 Binary files /dev/null and b/assets/SLA_Assets/Skills/Medical.png differ diff --git a/assets/SLA_Assets/Skills/Melee 2H Blade.png b/assets/SLA_Assets/Skills/Melee 2H Blade.png new file mode 100644 index 0000000..18d59b3 Binary files /dev/null and b/assets/SLA_Assets/Skills/Melee 2H Blade.png differ diff --git a/assets/SLA_Assets/Skills/Melee Axe.png b/assets/SLA_Assets/Skills/Melee Axe.png new file mode 100644 index 0000000..2f9b9fd Binary files /dev/null and b/assets/SLA_Assets/Skills/Melee Axe.png differ diff --git a/assets/SLA_Assets/Skills/Melee Blade 1H.png b/assets/SLA_Assets/Skills/Melee Blade 1H.png new file mode 100644 index 0000000..e9b7c8f Binary files /dev/null and b/assets/SLA_Assets/Skills/Melee Blade 1H.png differ diff --git a/assets/SLA_Assets/Skills/Melee Club.png b/assets/SLA_Assets/Skills/Melee Club.png new file mode 100644 index 0000000..62186c1 Binary files /dev/null and b/assets/SLA_Assets/Skills/Melee Club.png differ diff --git a/assets/SLA_Assets/Skills/Persuade.png b/assets/SLA_Assets/Skills/Persuade.png new file mode 100644 index 0000000..54a4573 Binary files /dev/null and b/assets/SLA_Assets/Skills/Persuade.png differ diff --git a/assets/SLA_Assets/Skills/Pilot.png b/assets/SLA_Assets/Skills/Pilot.png new file mode 100644 index 0000000..eed9ee6 Binary files /dev/null and b/assets/SLA_Assets/Skills/Pilot.png differ diff --git a/assets/SLA_Assets/Skills/Psychology.png b/assets/SLA_Assets/Skills/Psychology.png new file mode 100644 index 0000000..5ebcffb Binary files /dev/null and b/assets/SLA_Assets/Skills/Psychology.png differ diff --git a/assets/SLA_Assets/Skills/Rech Mechanical.png b/assets/SLA_Assets/Skills/Rech Mechanical.png new file mode 100644 index 0000000..cf4cdb4 Binary files /dev/null and b/assets/SLA_Assets/Skills/Rech Mechanical.png differ diff --git a/assets/SLA_Assets/Skills/SLA Info.png b/assets/SLA_Assets/Skills/SLA Info.png new file mode 100644 index 0000000..7188ef9 Binary files /dev/null and b/assets/SLA_Assets/Skills/SLA Info.png differ diff --git a/assets/SLA_Assets/Skills/Spot Hidden.png b/assets/SLA_Assets/Skills/Spot Hidden.png new file mode 100644 index 0000000..2841353 Binary files /dev/null and b/assets/SLA_Assets/Skills/Spot Hidden.png differ diff --git a/assets/SLA_Assets/Skills/Stealth.png b/assets/SLA_Assets/Skills/Stealth.png new file mode 100644 index 0000000..a735a91 Binary files /dev/null and b/assets/SLA_Assets/Skills/Stealth.png differ diff --git a/assets/SLA_Assets/Skills/Streetwise.png b/assets/SLA_Assets/Skills/Streetwise.png new file mode 100644 index 0000000..2430b62 Binary files /dev/null and b/assets/SLA_Assets/Skills/Streetwise.png differ diff --git a/assets/SLA_Assets/Skills/Swim.png b/assets/SLA_Assets/Skills/Swim.png new file mode 100644 index 0000000..a67505f Binary files /dev/null and b/assets/SLA_Assets/Skills/Swim.png differ diff --git a/assets/SLA_Assets/Skills/THrow.png b/assets/SLA_Assets/Skills/THrow.png new file mode 100644 index 0000000..d0f6bef Binary files /dev/null and b/assets/SLA_Assets/Skills/THrow.png differ diff --git a/assets/SLA_Assets/Skills/Tactics.png b/assets/SLA_Assets/Skills/Tactics.png new file mode 100644 index 0000000..7deb3f1 Binary files /dev/null and b/assets/SLA_Assets/Skills/Tactics.png differ diff --git a/assets/SLA_Assets/Skills/Tech Computers and AI.png b/assets/SLA_Assets/Skills/Tech Computers and AI.png new file mode 100644 index 0000000..bdadfbd Binary files /dev/null and b/assets/SLA_Assets/Skills/Tech Computers and AI.png differ diff --git a/assets/SLA_Assets/Skills/Tech Electronics.png b/assets/SLA_Assets/Skills/Tech Electronics.png new file mode 100644 index 0000000..11e71e3 Binary files /dev/null and b/assets/SLA_Assets/Skills/Tech Electronics.png differ diff --git a/assets/SLA_Assets/Skills/Tech Mechanical.png b/assets/SLA_Assets/Skills/Tech Mechanical.png new file mode 100644 index 0000000..d150a6a Binary files /dev/null and b/assets/SLA_Assets/Skills/Tech Mechanical.png differ diff --git a/assets/SLA_Assets/Skills/Tech Military.png b/assets/SLA_Assets/Skills/Tech Military.png new file mode 100644 index 0000000..564c205 Binary files /dev/null and b/assets/SLA_Assets/Skills/Tech Military.png differ diff --git a/assets/SLA_Assets/Special/Code of Honor.png b/assets/SLA_Assets/Special/Code of Honor.png new file mode 100644 index 0000000..fd0f97f Binary files /dev/null and b/assets/SLA_Assets/Special/Code of Honor.png differ diff --git a/assets/SLA_Assets/Special/Cold Adapted.png b/assets/SLA_Assets/Special/Cold Adapted.png new file mode 100644 index 0000000..c356d38 Binary files /dev/null and b/assets/SLA_Assets/Special/Cold Adapted.png differ diff --git a/assets/SLA_Assets/Special/Combat Drug User.png b/assets/SLA_Assets/Special/Combat Drug User.png new file mode 100644 index 0000000..46b8444 Binary files /dev/null and b/assets/SLA_Assets/Special/Combat Drug User.png differ diff --git a/assets/SLA_Assets/Special/Compassion.png b/assets/SLA_Assets/Special/Compassion.png new file mode 100644 index 0000000..0f4fb13 Binary files /dev/null and b/assets/SLA_Assets/Special/Compassion.png differ diff --git a/assets/SLA_Assets/Special/Ebb Fury.png b/assets/SLA_Assets/Special/Ebb Fury.png new file mode 100644 index 0000000..c558584 Binary files /dev/null and b/assets/SLA_Assets/Special/Ebb Fury.png differ diff --git a/assets/SLA_Assets/Special/Ebb Sensitive.png b/assets/SLA_Assets/Special/Ebb Sensitive.png new file mode 100644 index 0000000..e4da274 Binary files /dev/null and b/assets/SLA_Assets/Special/Ebb Sensitive.png differ diff --git a/assets/SLA_Assets/Special/Fast Reflexes.png b/assets/SLA_Assets/Special/Fast Reflexes.png new file mode 100644 index 0000000..0055459 Binary files /dev/null and b/assets/SLA_Assets/Special/Fast Reflexes.png differ diff --git a/assets/SLA_Assets/Special/Field Surgeon.png b/assets/SLA_Assets/Special/Field Surgeon.png new file mode 100644 index 0000000..beecb83 Binary files /dev/null and b/assets/SLA_Assets/Special/Field Surgeon.png differ diff --git a/assets/SLA_Assets/Special/Gut Feeling.png b/assets/SLA_Assets/Special/Gut Feeling.png new file mode 100644 index 0000000..0319410 Binary files /dev/null and b/assets/SLA_Assets/Special/Gut Feeling.png differ diff --git a/assets/SLA_Assets/Special/Hard Hitting.png b/assets/SLA_Assets/Special/Hard Hitting.png new file mode 100644 index 0000000..5bb7b4c Binary files /dev/null and b/assets/SLA_Assets/Special/Hard Hitting.png differ diff --git a/assets/SLA_Assets/Special/Healer.png b/assets/SLA_Assets/Special/Healer.png new file mode 100644 index 0000000..ebf3a57 Binary files /dev/null and b/assets/SLA_Assets/Special/Healer.png differ diff --git a/assets/SLA_Assets/Special/Inquisitive.png b/assets/SLA_Assets/Special/Inquisitive.png new file mode 100644 index 0000000..824f1d5 Binary files /dev/null and b/assets/SLA_Assets/Special/Inquisitive.png differ diff --git a/assets/SLA_Assets/Special/Menacing.png b/assets/SLA_Assets/Special/Menacing.png new file mode 100644 index 0000000..6094520 Binary files /dev/null and b/assets/SLA_Assets/Special/Menacing.png differ diff --git a/assets/SLA_Assets/Special/Regeneration.png b/assets/SLA_Assets/Special/Regeneration.png new file mode 100644 index 0000000..5cbeeaa Binary files /dev/null and b/assets/SLA_Assets/Special/Regeneration.png differ diff --git a/assets/SLA_Assets/Special/Sniper.png b/assets/SLA_Assets/Special/Sniper.png new file mode 100644 index 0000000..0c291c9 Binary files /dev/null and b/assets/SLA_Assets/Special/Sniper.png differ diff --git a/assets/SLA_Assets/Special/True Grit.png b/assets/SLA_Assets/Special/True Grit.png new file mode 100644 index 0000000..35ebf71 Binary files /dev/null and b/assets/SLA_Assets/Special/True Grit.png differ diff --git a/assets/SLA_Assets/Species/Advanced Carrion.png b/assets/SLA_Assets/Species/Advanced Carrion.png new file mode 100644 index 0000000..c48da90 Binary files /dev/null and b/assets/SLA_Assets/Species/Advanced Carrion.png differ diff --git a/assets/SLA_Assets/Species/Brain Waster.png b/assets/SLA_Assets/Species/Brain Waster.png new file mode 100644 index 0000000..70cb6c4 Binary files /dev/null and b/assets/SLA_Assets/Species/Brain Waster.png differ diff --git a/assets/SLA_Assets/Species/Ebon.png b/assets/SLA_Assets/Species/Ebon.png new file mode 100644 index 0000000..3d2e757 Binary files /dev/null and b/assets/SLA_Assets/Species/Ebon.png differ diff --git a/assets/SLA_Assets/Species/Frother.png b/assets/SLA_Assets/Species/Frother.png new file mode 100644 index 0000000..ed5a048 Binary files /dev/null and b/assets/SLA_Assets/Species/Frother.png differ diff --git a/assets/SLA_Assets/Species/Human.png b/assets/SLA_Assets/Species/Human.png new file mode 100644 index 0000000..40257d1 Binary files /dev/null and b/assets/SLA_Assets/Species/Human.png differ diff --git a/assets/SLA_Assets/Species/Shaktar.png b/assets/SLA_Assets/Species/Shaktar.png new file mode 100644 index 0000000..0e5261d Binary files /dev/null and b/assets/SLA_Assets/Species/Shaktar.png differ diff --git a/assets/SLA_Assets/Species/Stormer 313.png b/assets/SLA_Assets/Species/Stormer 313.png new file mode 100644 index 0000000..425b979 Binary files /dev/null and b/assets/SLA_Assets/Species/Stormer 313.png differ diff --git a/assets/SLA_Assets/Species/Stormer 711.png b/assets/SLA_Assets/Species/Stormer 711.png new file mode 100644 index 0000000..41746fd Binary files /dev/null and b/assets/SLA_Assets/Species/Stormer 711.png differ diff --git a/assets/SLA_Assets/Species/Wraith Raider.png b/assets/SLA_Assets/Species/Wraith Raider.png new file mode 100644 index 0000000..f7af115 Binary files /dev/null and b/assets/SLA_Assets/Species/Wraith Raider.png differ diff --git a/assets/SLA_Assets/Traits/addiction-compulsion.png b/assets/SLA_Assets/Traits/addiction-compulsion.png new file mode 100644 index 0000000..d275df0 Binary files /dev/null and b/assets/SLA_Assets/Traits/addiction-compulsion.png differ diff --git a/assets/SLA_Assets/Traits/allergy.png b/assets/SLA_Assets/Traits/allergy.png new file mode 100644 index 0000000..e987734 Binary files /dev/null and b/assets/SLA_Assets/Traits/allergy.png differ diff --git a/assets/SLA_Assets/Traits/ambidextrous.png b/assets/SLA_Assets/Traits/ambidextrous.png new file mode 100644 index 0000000..751e4e0 Binary files /dev/null and b/assets/SLA_Assets/Traits/ambidextrous.png differ diff --git a/assets/SLA_Assets/Traits/anger.png b/assets/SLA_Assets/Traits/anger.png new file mode 100644 index 0000000..a2b339a Binary files /dev/null and b/assets/SLA_Assets/Traits/anger.png differ diff --git a/assets/SLA_Assets/Traits/anxiety.png b/assets/SLA_Assets/Traits/anxiety.png new file mode 100644 index 0000000..f2ed3d6 Binary files /dev/null and b/assets/SLA_Assets/Traits/anxiety.png differ diff --git a/assets/SLA_Assets/Traits/arrogant.png b/assets/SLA_Assets/Traits/arrogant.png new file mode 100644 index 0000000..ce54a36 Binary files /dev/null and b/assets/SLA_Assets/Traits/arrogant.png differ diff --git a/assets/SLA_Assets/Traits/attractive.png b/assets/SLA_Assets/Traits/attractive.png new file mode 100644 index 0000000..142957a Binary files /dev/null and b/assets/SLA_Assets/Traits/attractive.png differ diff --git a/assets/SLA_Assets/Traits/chicken.png b/assets/SLA_Assets/Traits/chicken.png new file mode 100644 index 0000000..c815e7a Binary files /dev/null and b/assets/SLA_Assets/Traits/chicken.png differ diff --git a/assets/SLA_Assets/Traits/contact.png b/assets/SLA_Assets/Traits/contact.png new file mode 100644 index 0000000..3a06c12 Binary files /dev/null and b/assets/SLA_Assets/Traits/contact.png differ diff --git a/assets/SLA_Assets/Traits/debt.png b/assets/SLA_Assets/Traits/debt.png new file mode 100644 index 0000000..a0c5edf Binary files /dev/null and b/assets/SLA_Assets/Traits/debt.png differ diff --git a/assets/SLA_Assets/Traits/depression.png b/assets/SLA_Assets/Traits/depression.png new file mode 100644 index 0000000..d21c6d6 Binary files /dev/null and b/assets/SLA_Assets/Traits/depression.png differ diff --git a/assets/SLA_Assets/Traits/drug-addict.png b/assets/SLA_Assets/Traits/drug-addict.png new file mode 100644 index 0000000..3c47ef7 Binary files /dev/null and b/assets/SLA_Assets/Traits/drug-addict.png differ diff --git a/assets/SLA_Assets/Traits/enemy.png b/assets/SLA_Assets/Traits/enemy.png new file mode 100644 index 0000000..b989b47 Binary files /dev/null and b/assets/SLA_Assets/Traits/enemy.png differ diff --git a/assets/SLA_Assets/Traits/exceedingly-cool.png b/assets/SLA_Assets/Traits/exceedingly-cool.png new file mode 100644 index 0000000..b9ebdd1 Binary files /dev/null and b/assets/SLA_Assets/Traits/exceedingly-cool.png differ diff --git a/assets/SLA_Assets/Traits/good-hearing.png b/assets/SLA_Assets/Traits/good-hearing.png new file mode 100644 index 0000000..bf46057 Binary files /dev/null and b/assets/SLA_Assets/Traits/good-hearing.png differ diff --git a/assets/SLA_Assets/Traits/good-housing.png b/assets/SLA_Assets/Traits/good-housing.png new file mode 100644 index 0000000..7cb5048 Binary files /dev/null and b/assets/SLA_Assets/Traits/good-housing.png differ diff --git a/assets/SLA_Assets/Traits/good-vision.png b/assets/SLA_Assets/Traits/good-vision.png new file mode 100644 index 0000000..7b1d468 Binary files /dev/null and b/assets/SLA_Assets/Traits/good-vision.png differ diff --git a/assets/SLA_Assets/Traits/illness.png b/assets/SLA_Assets/Traits/illness.png new file mode 100644 index 0000000..cc97d3c Binary files /dev/null and b/assets/SLA_Assets/Traits/illness.png differ diff --git a/assets/SLA_Assets/Traits/natural-aptitude-skill.png b/assets/SLA_Assets/Traits/natural-aptitude-skill.png new file mode 100644 index 0000000..717d714 Binary files /dev/null and b/assets/SLA_Assets/Traits/natural-aptitude-skill.png differ diff --git a/assets/SLA_Assets/Traits/natural-aptitude-stat.png b/assets/SLA_Assets/Traits/natural-aptitude-stat.png new file mode 100644 index 0000000..f0b1907 Binary files /dev/null and b/assets/SLA_Assets/Traits/natural-aptitude-stat.png differ diff --git a/assets/SLA_Assets/Traits/pacifist.png b/assets/SLA_Assets/Traits/pacifist.png new file mode 100644 index 0000000..21df87c Binary files /dev/null and b/assets/SLA_Assets/Traits/pacifist.png differ diff --git a/assets/SLA_Assets/Traits/phobia.png b/assets/SLA_Assets/Traits/phobia.png new file mode 100644 index 0000000..a8d94f4 Binary files /dev/null and b/assets/SLA_Assets/Traits/phobia.png differ diff --git a/assets/SLA_Assets/Traits/poor-hearing.png b/assets/SLA_Assets/Traits/poor-hearing.png new file mode 100644 index 0000000..1805920 Binary files /dev/null and b/assets/SLA_Assets/Traits/poor-hearing.png differ diff --git a/assets/SLA_Assets/Traits/poor-housing.png b/assets/SLA_Assets/Traits/poor-housing.png new file mode 100644 index 0000000..aa75c7a Binary files /dev/null and b/assets/SLA_Assets/Traits/poor-housing.png differ diff --git a/assets/SLA_Assets/Traits/poor-vision.png b/assets/SLA_Assets/Traits/poor-vision.png new file mode 100644 index 0000000..07748dc Binary files /dev/null and b/assets/SLA_Assets/Traits/poor-vision.png differ diff --git a/assets/SLA_Assets/Traits/psychosis.png b/assets/SLA_Assets/Traits/psychosis.png new file mode 100644 index 0000000..9acb50a Binary files /dev/null and b/assets/SLA_Assets/Traits/psychosis.png differ diff --git a/assets/SLA_Assets/Traits/savings.png b/assets/SLA_Assets/Traits/savings.png new file mode 100644 index 0000000..23f3f21 Binary files /dev/null and b/assets/SLA_Assets/Traits/savings.png differ diff --git a/assets/SLA_Assets/Traits/sterile.png b/assets/SLA_Assets/Traits/sterile.png new file mode 100644 index 0000000..8a721f3 Binary files /dev/null and b/assets/SLA_Assets/Traits/sterile.png differ diff --git a/assets/SLA_Assets/Traits/unattractive.png b/assets/SLA_Assets/Traits/unattractive.png new file mode 100644 index 0000000..03ee59c Binary files /dev/null and b/assets/SLA_Assets/Traits/unattractive.png differ diff --git a/assets/SLA_Assets/Weapons/Assult Hammer.png b/assets/SLA_Assets/Weapons/Assult Hammer.png new file mode 100644 index 0000000..13a8f71 Binary files /dev/null and b/assets/SLA_Assets/Weapons/Assult Hammer.png differ diff --git a/assets/SLA_Assets/Weapons/BLA Blitzer.png b/assets/SLA_Assets/Weapons/BLA Blitzer.png new file mode 100644 index 0000000..9ab366e Binary files /dev/null and b/assets/SLA_Assets/Weapons/BLA Blitzer.png differ diff --git a/assets/SLA_Assets/Weapons/CAF Blade.png b/assets/SLA_Assets/Weapons/CAF Blade.png new file mode 100644 index 0000000..34b7e0b Binary files /dev/null and b/assets/SLA_Assets/Weapons/CAF Blade.png differ diff --git a/assets/SLA_Assets/Weapons/CAF SMG.png b/assets/SLA_Assets/Weapons/CAF SMG.png new file mode 100644 index 0000000..fb82fd3 Binary files /dev/null and b/assets/SLA_Assets/Weapons/CAF SMG.png differ diff --git a/assets/SLA_Assets/Weapons/Carrien Sword.png b/assets/SLA_Assets/Weapons/Carrien Sword.png new file mode 100644 index 0000000..98e0a51 Binary files /dev/null and b/assets/SLA_Assets/Weapons/Carrien Sword.png differ diff --git a/assets/SLA_Assets/Weapons/Chain Glaive.png b/assets/SLA_Assets/Weapons/Chain Glaive.png new file mode 100644 index 0000000..307059a Binary files /dev/null and b/assets/SLA_Assets/Weapons/Chain Glaive.png differ diff --git a/assets/SLA_Assets/Weapons/Chain Knife.png b/assets/SLA_Assets/Weapons/Chain Knife.png new file mode 100644 index 0000000..77e6e78 Binary files /dev/null and b/assets/SLA_Assets/Weapons/Chain Knife.png differ diff --git a/assets/SLA_Assets/Weapons/Chainaxe1.png b/assets/SLA_Assets/Weapons/Chainaxe1.png new file mode 100644 index 0000000..73d97b6 Binary files /dev/null and b/assets/SLA_Assets/Weapons/Chainaxe1.png differ diff --git a/assets/SLA_Assets/Weapons/Chainsaw1.png b/assets/SLA_Assets/Weapons/Chainsaw1.png new file mode 100644 index 0000000..a475ace Binary files /dev/null and b/assets/SLA_Assets/Weapons/Chainsaw1.png differ diff --git a/assets/SLA_Assets/Weapons/Colapsable Baton.png b/assets/SLA_Assets/Weapons/Colapsable Baton.png new file mode 100644 index 0000000..4c1ce1a Binary files /dev/null and b/assets/SLA_Assets/Weapons/Colapsable Baton.png differ diff --git a/assets/SLA_Assets/Weapons/Ebb Infused Blade.png b/assets/SLA_Assets/Weapons/Ebb Infused Blade.png new file mode 100644 index 0000000..4b054e2 Binary files /dev/null and b/assets/SLA_Assets/Weapons/Ebb Infused Blade.png differ diff --git a/assets/SLA_Assets/Weapons/FEN 204 Gunhead.png b/assets/SLA_Assets/Weapons/FEN 204 Gunhead.png new file mode 100644 index 0000000..5af1d84 Binary files /dev/null and b/assets/SLA_Assets/Weapons/FEN 204 Gunhead.png differ diff --git a/assets/SLA_Assets/Weapons/FEN 204.png b/assets/SLA_Assets/Weapons/FEN 204.png new file mode 100644 index 0000000..d7cf32b Binary files /dev/null and b/assets/SLA_Assets/Weapons/FEN 204.png differ diff --git a/assets/SLA_Assets/Weapons/FEN 209 Machine PIstol.png b/assets/SLA_Assets/Weapons/FEN 209 Machine PIstol.png new file mode 100644 index 0000000..43fab81 Binary files /dev/null and b/assets/SLA_Assets/Weapons/FEN 209 Machine PIstol.png differ diff --git a/assets/SLA_Assets/Weapons/FEN 30-30.png b/assets/SLA_Assets/Weapons/FEN 30-30.png new file mode 100644 index 0000000..28f7e6f Binary files /dev/null and b/assets/SLA_Assets/Weapons/FEN 30-30.png differ diff --git a/assets/SLA_Assets/Weapons/FEN 401 Shiver Pistol.png b/assets/SLA_Assets/Weapons/FEN 401 Shiver Pistol.png new file mode 100644 index 0000000..bace25f Binary files /dev/null and b/assets/SLA_Assets/Weapons/FEN 401 Shiver Pistol.png differ diff --git a/assets/SLA_Assets/Weapons/FEN 603 Auto Pistol.png b/assets/SLA_Assets/Weapons/FEN 603 Auto Pistol.png new file mode 100644 index 0000000..752362d Binary files /dev/null and b/assets/SLA_Assets/Weapons/FEN 603 Auto Pistol.png differ diff --git a/assets/SLA_Assets/Weapons/FEN 603.png b/assets/SLA_Assets/Weapons/FEN 603.png new file mode 100644 index 0000000..c10de64 Binary files /dev/null and b/assets/SLA_Assets/Weapons/FEN 603.png differ diff --git a/assets/SLA_Assets/Weapons/FEN 703 Heavy Pistol.png b/assets/SLA_Assets/Weapons/FEN 703 Heavy Pistol.png new file mode 100644 index 0000000..b393ed3 Binary files /dev/null and b/assets/SLA_Assets/Weapons/FEN 703 Heavy Pistol.png differ diff --git a/assets/SLA_Assets/Weapons/FEN 706.png b/assets/SLA_Assets/Weapons/FEN 706.png new file mode 100644 index 0000000..70e64d1 Binary files /dev/null and b/assets/SLA_Assets/Weapons/FEN 706.png differ diff --git a/assets/SLA_Assets/Weapons/FEN 981 Sniper Rifle.png b/assets/SLA_Assets/Weapons/FEN 981 Sniper Rifle.png new file mode 100644 index 0000000..b41de39 Binary files /dev/null and b/assets/SLA_Assets/Weapons/FEN 981 Sniper Rifle.png differ diff --git a/assets/SLA_Assets/Weapons/FEN AR Assault Rifle.png b/assets/SLA_Assets/Weapons/FEN AR Assault Rifle.png new file mode 100644 index 0000000..6dc0a08 Binary files /dev/null and b/assets/SLA_Assets/Weapons/FEN AR Assault Rifle.png differ diff --git a/assets/SLA_Assets/Weapons/FEN AR.png b/assets/SLA_Assets/Weapons/FEN AR.png new file mode 100644 index 0000000..19a0b62 Binary files /dev/null and b/assets/SLA_Assets/Weapons/FEN AR.png differ diff --git a/assets/SLA_Assets/Weapons/FEN Reaper LMG.png b/assets/SLA_Assets/Weapons/FEN Reaper LMG.png new file mode 100644 index 0000000..1afaef9 Binary files /dev/null and b/assets/SLA_Assets/Weapons/FEN Reaper LMG.png differ diff --git a/assets/SLA_Assets/Weapons/FEN10 Street Sweeper.png b/assets/SLA_Assets/Weapons/FEN10 Street Sweeper.png new file mode 100644 index 0000000..ea8e250 Binary files /dev/null and b/assets/SLA_Assets/Weapons/FEN10 Street Sweeper.png differ diff --git a/assets/SLA_Assets/Weapons/FEN701 Urban Carbine.png b/assets/SLA_Assets/Weapons/FEN701 Urban Carbine.png new file mode 100644 index 0000000..9f9bb67 Binary files /dev/null and b/assets/SLA_Assets/Weapons/FEN701 Urban Carbine.png differ diff --git a/assets/SLA_Assets/Weapons/Flick Syth.png b/assets/SLA_Assets/Weapons/Flick Syth.png new file mode 100644 index 0000000..2a4cbaa Binary files /dev/null and b/assets/SLA_Assets/Weapons/Flick Syth.png differ diff --git a/assets/SLA_Assets/Weapons/Frag Grenade.png b/assets/SLA_Assets/Weapons/Frag Grenade.png new file mode 100644 index 0000000..0fa916b Binary files /dev/null and b/assets/SLA_Assets/Weapons/Frag Grenade.png differ diff --git a/assets/SLA_Assets/Weapons/GA 50 Finisher.png b/assets/SLA_Assets/Weapons/GA 50 Finisher.png new file mode 100644 index 0000000..6dc8627 Binary files /dev/null and b/assets/SLA_Assets/Weapons/GA 50 Finisher.png differ diff --git a/assets/SLA_Assets/Weapons/GA 9442.png b/assets/SLA_Assets/Weapons/GA 9442.png new file mode 100644 index 0000000..6e7e6de Binary files /dev/null and b/assets/SLA_Assets/Weapons/GA 9442.png differ diff --git a/assets/SLA_Assets/Weapons/GA 9443.png b/assets/SLA_Assets/Weapons/GA 9443.png new file mode 100644 index 0000000..bc348a1 Binary files /dev/null and b/assets/SLA_Assets/Weapons/GA 9443.png differ diff --git a/assets/SLA_Assets/Weapons/GASH First.png b/assets/SLA_Assets/Weapons/GASH First.png new file mode 100644 index 0000000..817fa47 Binary files /dev/null and b/assets/SLA_Assets/Weapons/GASH First.png differ diff --git a/assets/SLA_Assets/Weapons/GASH Pacifier Baton.png b/assets/SLA_Assets/Weapons/GASH Pacifier Baton.png new file mode 100644 index 0000000..4550e99 Binary files /dev/null and b/assets/SLA_Assets/Weapons/GASH Pacifier Baton.png differ diff --git a/assets/SLA_Assets/Weapons/Garrotte Wire.png b/assets/SLA_Assets/Weapons/Garrotte Wire.png new file mode 100644 index 0000000..0b33825 Binary files /dev/null and b/assets/SLA_Assets/Weapons/Garrotte Wire.png differ diff --git a/assets/SLA_Assets/Weapons/KPS Mangler .png b/assets/SLA_Assets/Weapons/KPS Mangler .png new file mode 100644 index 0000000..b113551 Binary files /dev/null and b/assets/SLA_Assets/Weapons/KPS Mangler .png differ diff --git a/assets/SLA_Assets/Weapons/KPS Shotgun.png b/assets/SLA_Assets/Weapons/KPS Shotgun.png new file mode 100644 index 0000000..b40cba1 Binary files /dev/null and b/assets/SLA_Assets/Weapons/KPS Shotgun.png differ diff --git a/assets/SLA_Assets/Weapons/MAC Knife.png b/assets/SLA_Assets/Weapons/MAC Knife.png new file mode 100644 index 0000000..e58d164 Binary files /dev/null and b/assets/SLA_Assets/Weapons/MAC Knife.png differ diff --git a/assets/SLA_Assets/Weapons/MILA Grenage Launcher.png b/assets/SLA_Assets/Weapons/MILA Grenage Launcher.png new file mode 100644 index 0000000..ea5bcd7 Binary files /dev/null and b/assets/SLA_Assets/Weapons/MILA Grenage Launcher.png differ diff --git a/assets/SLA_Assets/Weapons/Mono Katana.png b/assets/SLA_Assets/Weapons/Mono Katana.png new file mode 100644 index 0000000..cb93de6 Binary files /dev/null and b/assets/SLA_Assets/Weapons/Mono Katana.png differ diff --git a/assets/SLA_Assets/Weapons/Pacifier Baton.png b/assets/SLA_Assets/Weapons/Pacifier Baton.png new file mode 100644 index 0000000..f283624 Binary files /dev/null and b/assets/SLA_Assets/Weapons/Pacifier Baton.png differ diff --git a/assets/SLA_Assets/Weapons/Power ClayMOre.png b/assets/SLA_Assets/Weapons/Power ClayMOre.png new file mode 100644 index 0000000..ebbca3b Binary files /dev/null and b/assets/SLA_Assets/Weapons/Power ClayMOre.png differ diff --git a/assets/SLA_Assets/Weapons/Powerclaymore.png b/assets/SLA_Assets/Weapons/Powerclaymore.png new file mode 100644 index 0000000..d8a59e1 Binary files /dev/null and b/assets/SLA_Assets/Weapons/Powerclaymore.png differ diff --git a/assets/SLA_Assets/Weapons/SLA Blade.png b/assets/SLA_Assets/Weapons/SLA Blade.png new file mode 100644 index 0000000..1e2ad10 Binary files /dev/null and b/assets/SLA_Assets/Weapons/SLA Blade.png differ diff --git a/assets/SLA_Assets/Weapons/SLA Breach.png b/assets/SLA_Assets/Weapons/SLA Breach.png new file mode 100644 index 0000000..3e72099 Binary files /dev/null and b/assets/SLA_Assets/Weapons/SLA Breach.png differ diff --git a/assets/SLA_Assets/Weapons/SLA Carbine Rifle.png b/assets/SLA_Assets/Weapons/SLA Carbine Rifle.png new file mode 100644 index 0000000..8b96ad1 Binary files /dev/null and b/assets/SLA_Assets/Weapons/SLA Carbine Rifle.png differ diff --git a/assets/SLA_Assets/Weapons/SLA Combat KNife Standard.png b/assets/SLA_Assets/Weapons/SLA Combat KNife Standard.png new file mode 100644 index 0000000..07f8404 Binary files /dev/null and b/assets/SLA_Assets/Weapons/SLA Combat KNife Standard.png differ diff --git a/assets/SLA_Assets/Weapons/SLA Combat Shotgun.png b/assets/SLA_Assets/Weapons/SLA Combat Shotgun.png new file mode 100644 index 0000000..d242c51 Binary files /dev/null and b/assets/SLA_Assets/Weapons/SLA Combat Shotgun.png differ diff --git a/assets/SLA_Assets/Weapons/SLA Heavy Launcher.png b/assets/SLA_Assets/Weapons/SLA Heavy Launcher.png new file mode 100644 index 0000000..39cd448 Binary files /dev/null and b/assets/SLA_Assets/Weapons/SLA Heavy Launcher.png differ diff --git a/assets/SLA_Assets/Weapons/SLA MArksman Rifle.png b/assets/SLA_Assets/Weapons/SLA MArksman Rifle.png new file mode 100644 index 0000000..158f323 Binary files /dev/null and b/assets/SLA_Assets/Weapons/SLA MArksman Rifle.png differ diff --git a/assets/SLA_Assets/Weapons/SLA Mono Knife.png b/assets/SLA_Assets/Weapons/SLA Mono Knife.png new file mode 100644 index 0000000..3d0b270 Binary files /dev/null and b/assets/SLA_Assets/Weapons/SLA Mono Knife.png differ diff --git a/assets/SLA_Assets/Weapons/SLA SUpport LMG.png b/assets/SLA_Assets/Weapons/SLA SUpport LMG.png new file mode 100644 index 0000000..d5abb1c Binary files /dev/null and b/assets/SLA_Assets/Weapons/SLA SUpport LMG.png differ diff --git a/assets/SLA_Assets/Weapons/SLA Snare Launcher.png b/assets/SLA_Assets/Weapons/SLA Snare Launcher.png new file mode 100644 index 0000000..679d318 Binary files /dev/null and b/assets/SLA_Assets/Weapons/SLA Snare Launcher.png differ diff --git a/assets/SLA_Assets/Weapons/Shacktar Claws.png b/assets/SLA_Assets/Weapons/Shacktar Claws.png new file mode 100644 index 0000000..3daa78a Binary files /dev/null and b/assets/SLA_Assets/Weapons/Shacktar Claws.png differ diff --git a/assets/SLA_Assets/Weapons/Spike Gauntlet.png b/assets/SLA_Assets/Weapons/Spike Gauntlet.png new file mode 100644 index 0000000..2cfb087 Binary files /dev/null and b/assets/SLA_Assets/Weapons/Spike Gauntlet.png differ diff --git a/assets/SLA_Assets/Weapons/Stormer Claws.png b/assets/SLA_Assets/Weapons/Stormer Claws.png new file mode 100644 index 0000000..2066b51 Binary files /dev/null and b/assets/SLA_Assets/Weapons/Stormer Claws.png differ diff --git a/assets/SLA_Assets/Weapons/Vibro Sabre.png b/assets/SLA_Assets/Weapons/Vibro Sabre.png new file mode 100644 index 0000000..e4d2824 Binary files /dev/null and b/assets/SLA_Assets/Weapons/Vibro Sabre.png differ diff --git a/assets/SLA_Assets/Weapons/Wraith Raider Claws.png b/assets/SLA_Assets/Weapons/Wraith Raider Claws.png new file mode 100644 index 0000000..d37ce61 Binary files /dev/null and b/assets/SLA_Assets/Weapons/Wraith Raider Claws.png differ diff --git a/assets/blade-runner-banner-small.webp b/assets/blade-runner-banner-small.webp new file mode 100644 index 0000000..4743698 Binary files /dev/null and b/assets/blade-runner-banner-small.webp differ diff --git a/assets/blade-runner-cover.webp b/assets/blade-runner-cover.webp new file mode 100644 index 0000000..1b998de Binary files /dev/null and b/assets/blade-runner-cover.webp differ diff --git a/assets/dice/alien/alien-dice-b0.png b/assets/dice/alien/alien-dice-b0.png new file mode 100644 index 0000000..d8ebf14 Binary files /dev/null and b/assets/dice/alien/alien-dice-b0.png differ diff --git a/assets/dice/alien/alien-dice-b6.png b/assets/dice/alien/alien-dice-b6.png new file mode 100644 index 0000000..805f462 Binary files /dev/null and b/assets/dice/alien/alien-dice-b6.png differ diff --git a/assets/dice/alien/alien-dice-y0.png b/assets/dice/alien/alien-dice-y0.png new file mode 100644 index 0000000..f7744d0 Binary files /dev/null and b/assets/dice/alien/alien-dice-y0.png differ diff --git a/assets/dice/alien/alien-dice-y1.png b/assets/dice/alien/alien-dice-y1.png new file mode 100644 index 0000000..82dcc6f Binary files /dev/null and b/assets/dice/alien/alien-dice-y1.png differ diff --git a/assets/dice/alien/alien-dice-y6.png b/assets/dice/alien/alien-dice-y6.png new file mode 100644 index 0000000..001e78a Binary files /dev/null and b/assets/dice/alien/alien-dice-y6.png differ diff --git a/assets/dice/alien/b1.png b/assets/dice/alien/b1.png new file mode 100644 index 0000000..d321bb7 Binary files /dev/null and b/assets/dice/alien/b1.png differ diff --git a/assets/dice/alien/b2.png b/assets/dice/alien/b2.png new file mode 100644 index 0000000..759d6ae Binary files /dev/null and b/assets/dice/alien/b2.png differ diff --git a/assets/dice/alien/b3.png b/assets/dice/alien/b3.png new file mode 100644 index 0000000..6e64667 Binary files /dev/null and b/assets/dice/alien/b3.png differ diff --git a/assets/dice/alien/b4.png b/assets/dice/alien/b4.png new file mode 100644 index 0000000..2e151d8 Binary files /dev/null and b/assets/dice/alien/b4.png differ diff --git a/assets/dice/alien/b5.png b/assets/dice/alien/b5.png new file mode 100644 index 0000000..90a2eb0 Binary files /dev/null and b/assets/dice/alien/b5.png differ diff --git a/assets/dice/alien/y2.png b/assets/dice/alien/y2.png new file mode 100644 index 0000000..bc7f259 Binary files /dev/null and b/assets/dice/alien/y2.png differ diff --git a/assets/dice/alien/y3.png b/assets/dice/alien/y3.png new file mode 100644 index 0000000..341fc91 Binary files /dev/null and b/assets/dice/alien/y3.png differ diff --git a/assets/dice/alien/y4.png b/assets/dice/alien/y4.png new file mode 100644 index 0000000..9a1d9aa Binary files /dev/null and b/assets/dice/alien/y4.png differ diff --git a/assets/dice/alien/y5.png b/assets/dice/alien/y5.png new file mode 100644 index 0000000..541532b Binary files /dev/null and b/assets/dice/alien/y5.png differ diff --git a/assets/dice/d10/br_d10_1.png b/assets/dice/d10/br_d10_1.png new file mode 100644 index 0000000..92a7e23 Binary files /dev/null and b/assets/dice/d10/br_d10_1.png differ diff --git a/assets/dice/d10/br_d10_1.webp b/assets/dice/d10/br_d10_1.webp new file mode 100644 index 0000000..a23c2f7 Binary files /dev/null and b/assets/dice/d10/br_d10_1.webp differ diff --git a/assets/dice/d10/br_d10_10.png b/assets/dice/d10/br_d10_10.png new file mode 100644 index 0000000..5f2ab44 Binary files /dev/null and b/assets/dice/d10/br_d10_10.png differ diff --git a/assets/dice/d10/br_d10_10.webp b/assets/dice/d10/br_d10_10.webp new file mode 100644 index 0000000..38aa233 Binary files /dev/null and b/assets/dice/d10/br_d10_10.webp differ diff --git a/assets/dice/d10/br_d10_10_bump.webp b/assets/dice/d10/br_d10_10_bump.webp new file mode 100644 index 0000000..d3c1685 Binary files /dev/null and b/assets/dice/d10/br_d10_10_bump.webp differ diff --git a/assets/dice/d10/br_d10_1_bump.webp b/assets/dice/d10/br_d10_1_bump.webp new file mode 100644 index 0000000..2e2e795 Binary files /dev/null and b/assets/dice/d10/br_d10_1_bump.webp differ diff --git a/assets/dice/d10/br_d10_6.png b/assets/dice/d10/br_d10_6.png new file mode 100644 index 0000000..9c77ced Binary files /dev/null and b/assets/dice/d10/br_d10_6.png differ diff --git a/assets/dice/d10/br_d10_6.webp b/assets/dice/d10/br_d10_6.webp new file mode 100644 index 0000000..33ba45f Binary files /dev/null and b/assets/dice/d10/br_d10_6.webp differ diff --git a/assets/dice/d10/br_d10_6_bump.webp b/assets/dice/d10/br_d10_6_bump.webp new file mode 100644 index 0000000..8cf6fe5 Binary files /dev/null and b/assets/dice/d10/br_d10_6_bump.webp differ diff --git a/assets/dice/d10/br_d10_7.png b/assets/dice/d10/br_d10_7.png new file mode 100644 index 0000000..abcf54f Binary files /dev/null and b/assets/dice/d10/br_d10_7.png differ diff --git a/assets/dice/d10/br_d10_7.webp b/assets/dice/d10/br_d10_7.webp new file mode 100644 index 0000000..0db4403 Binary files /dev/null and b/assets/dice/d10/br_d10_7.webp differ diff --git a/assets/dice/d10/br_d10_7_bump.webp b/assets/dice/d10/br_d10_7_bump.webp new file mode 100644 index 0000000..2aff6ad Binary files /dev/null and b/assets/dice/d10/br_d10_7_bump.webp differ diff --git a/assets/dice/d10/br_d10_8.png b/assets/dice/d10/br_d10_8.png new file mode 100644 index 0000000..9aafb5d Binary files /dev/null and b/assets/dice/d10/br_d10_8.png differ diff --git a/assets/dice/d10/br_d10_8.webp b/assets/dice/d10/br_d10_8.webp new file mode 100644 index 0000000..a3744b9 Binary files /dev/null and b/assets/dice/d10/br_d10_8.webp differ diff --git a/assets/dice/d10/br_d10_8_bump.webp b/assets/dice/d10/br_d10_8_bump.webp new file mode 100644 index 0000000..b7d6dbc Binary files /dev/null and b/assets/dice/d10/br_d10_8_bump.webp differ diff --git a/assets/dice/d10/br_d10_9.png b/assets/dice/d10/br_d10_9.png new file mode 100644 index 0000000..fdaec00 Binary files /dev/null and b/assets/dice/d10/br_d10_9.png differ diff --git a/assets/dice/d10/br_d10_9.webp b/assets/dice/d10/br_d10_9.webp new file mode 100644 index 0000000..5d07e96 Binary files /dev/null and b/assets/dice/d10/br_d10_9.webp differ diff --git a/assets/dice/d10/br_d10_9_bump.webp b/assets/dice/d10/br_d10_9_bump.webp new file mode 100644 index 0000000..6daf0dd Binary files /dev/null and b/assets/dice/d10/br_d10_9_bump.webp differ diff --git a/assets/dice/d12/br_d12_1.png b/assets/dice/d12/br_d12_1.png new file mode 100644 index 0000000..7d17b46 Binary files /dev/null and b/assets/dice/d12/br_d12_1.png differ diff --git a/assets/dice/d12/br_d12_1.webp b/assets/dice/d12/br_d12_1.webp new file mode 100644 index 0000000..a8e112e Binary files /dev/null and b/assets/dice/d12/br_d12_1.webp differ diff --git a/assets/dice/d12/br_d12_10.png b/assets/dice/d12/br_d12_10.png new file mode 100644 index 0000000..b06d5c8 Binary files /dev/null and b/assets/dice/d12/br_d12_10.png differ diff --git a/assets/dice/d12/br_d12_10.webp b/assets/dice/d12/br_d12_10.webp new file mode 100644 index 0000000..e83ecef Binary files /dev/null and b/assets/dice/d12/br_d12_10.webp differ diff --git a/assets/dice/d12/br_d12_10_bump.webp b/assets/dice/d12/br_d12_10_bump.webp new file mode 100644 index 0000000..380db61 Binary files /dev/null and b/assets/dice/d12/br_d12_10_bump.webp differ diff --git a/assets/dice/d12/br_d12_11.png b/assets/dice/d12/br_d12_11.png new file mode 100644 index 0000000..7654836 Binary files /dev/null and b/assets/dice/d12/br_d12_11.png differ diff --git a/assets/dice/d12/br_d12_11.webp b/assets/dice/d12/br_d12_11.webp new file mode 100644 index 0000000..1a42394 Binary files /dev/null and b/assets/dice/d12/br_d12_11.webp differ diff --git a/assets/dice/d12/br_d12_11_bump.webp b/assets/dice/d12/br_d12_11_bump.webp new file mode 100644 index 0000000..4ba6067 Binary files /dev/null and b/assets/dice/d12/br_d12_11_bump.webp differ diff --git a/assets/dice/d12/br_d12_12.png b/assets/dice/d12/br_d12_12.png new file mode 100644 index 0000000..eae9e07 Binary files /dev/null and b/assets/dice/d12/br_d12_12.png differ diff --git a/assets/dice/d12/br_d12_12.webp b/assets/dice/d12/br_d12_12.webp new file mode 100644 index 0000000..9b8b22d Binary files /dev/null and b/assets/dice/d12/br_d12_12.webp differ diff --git a/assets/dice/d12/br_d12_12_bump.webp b/assets/dice/d12/br_d12_12_bump.webp new file mode 100644 index 0000000..fd6b2b6 Binary files /dev/null and b/assets/dice/d12/br_d12_12_bump.webp differ diff --git a/assets/dice/d12/br_d12_1_bump.webp b/assets/dice/d12/br_d12_1_bump.webp new file mode 100644 index 0000000..3b839df Binary files /dev/null and b/assets/dice/d12/br_d12_1_bump.webp differ diff --git a/assets/dice/d12/br_d12_6.png b/assets/dice/d12/br_d12_6.png new file mode 100644 index 0000000..a8bffac Binary files /dev/null and b/assets/dice/d12/br_d12_6.png differ diff --git a/assets/dice/d12/br_d12_6.webp b/assets/dice/d12/br_d12_6.webp new file mode 100644 index 0000000..e3f0300 Binary files /dev/null and b/assets/dice/d12/br_d12_6.webp differ diff --git a/assets/dice/d12/br_d12_6_bump.webp b/assets/dice/d12/br_d12_6_bump.webp new file mode 100644 index 0000000..2e56c04 Binary files /dev/null and b/assets/dice/d12/br_d12_6_bump.webp differ diff --git a/assets/dice/d12/br_d12_7.png b/assets/dice/d12/br_d12_7.png new file mode 100644 index 0000000..f5d5b9f Binary files /dev/null and b/assets/dice/d12/br_d12_7.png differ diff --git a/assets/dice/d12/br_d12_7.webp b/assets/dice/d12/br_d12_7.webp new file mode 100644 index 0000000..7b2d17c Binary files /dev/null and b/assets/dice/d12/br_d12_7.webp differ diff --git a/assets/dice/d12/br_d12_7_bump.webp b/assets/dice/d12/br_d12_7_bump.webp new file mode 100644 index 0000000..0303a05 Binary files /dev/null and b/assets/dice/d12/br_d12_7_bump.webp differ diff --git a/assets/dice/d12/br_d12_8.png b/assets/dice/d12/br_d12_8.png new file mode 100644 index 0000000..dfc4d0c Binary files /dev/null and b/assets/dice/d12/br_d12_8.png differ diff --git a/assets/dice/d12/br_d12_8.webp b/assets/dice/d12/br_d12_8.webp new file mode 100644 index 0000000..cade809 Binary files /dev/null and b/assets/dice/d12/br_d12_8.webp differ diff --git a/assets/dice/d12/br_d12_8_bump.webp b/assets/dice/d12/br_d12_8_bump.webp new file mode 100644 index 0000000..1dfeb3a Binary files /dev/null and b/assets/dice/d12/br_d12_8_bump.webp differ diff --git a/assets/dice/d12/br_d12_9.png b/assets/dice/d12/br_d12_9.png new file mode 100644 index 0000000..d7103ce Binary files /dev/null and b/assets/dice/d12/br_d12_9.png differ diff --git a/assets/dice/d12/br_d12_9.webp b/assets/dice/d12/br_d12_9.webp new file mode 100644 index 0000000..103948e Binary files /dev/null and b/assets/dice/d12/br_d12_9.webp differ diff --git a/assets/dice/d12/br_d12_9_bump.webp b/assets/dice/d12/br_d12_9_bump.webp new file mode 100644 index 0000000..49c994d Binary files /dev/null and b/assets/dice/d12/br_d12_9_bump.webp differ diff --git a/assets/dice/d6/br_d6_1.png b/assets/dice/d6/br_d6_1.png new file mode 100644 index 0000000..cff5a80 Binary files /dev/null and b/assets/dice/d6/br_d6_1.png differ diff --git a/assets/dice/d6/br_d6_1.webp b/assets/dice/d6/br_d6_1.webp new file mode 100644 index 0000000..3bc61c8 Binary files /dev/null and b/assets/dice/d6/br_d6_1.webp differ diff --git a/assets/dice/d6/br_d6_1_bump.webp b/assets/dice/d6/br_d6_1_bump.webp new file mode 100644 index 0000000..8f48210 Binary files /dev/null and b/assets/dice/d6/br_d6_1_bump.webp differ diff --git a/assets/dice/d6/br_d6_6.png b/assets/dice/d6/br_d6_6.png new file mode 100644 index 0000000..ab9333b Binary files /dev/null and b/assets/dice/d6/br_d6_6.png differ diff --git a/assets/dice/d6/br_d6_6.webp b/assets/dice/d6/br_d6_6.webp new file mode 100644 index 0000000..7be9dcc Binary files /dev/null and b/assets/dice/d6/br_d6_6.webp differ diff --git a/assets/dice/d6/br_d6_6_bump.webp b/assets/dice/d6/br_d6_6_bump.webp new file mode 100644 index 0000000..ab4dd55 Binary files /dev/null and b/assets/dice/d6/br_d6_6_bump.webp differ diff --git a/assets/dice/d8/br_d8_1.png b/assets/dice/d8/br_d8_1.png new file mode 100644 index 0000000..2c2f99f Binary files /dev/null and b/assets/dice/d8/br_d8_1.png differ diff --git a/assets/dice/d8/br_d8_1.webp b/assets/dice/d8/br_d8_1.webp new file mode 100644 index 0000000..d16c63d Binary files /dev/null and b/assets/dice/d8/br_d8_1.webp differ diff --git a/assets/dice/d8/br_d8_1_bump.webp b/assets/dice/d8/br_d8_1_bump.webp new file mode 100644 index 0000000..8464349 Binary files /dev/null and b/assets/dice/d8/br_d8_1_bump.webp differ diff --git a/assets/dice/d8/br_d8_6.png b/assets/dice/d8/br_d8_6.png new file mode 100644 index 0000000..a9db3df Binary files /dev/null and b/assets/dice/d8/br_d8_6.png differ diff --git a/assets/dice/d8/br_d8_6.webp b/assets/dice/d8/br_d8_6.webp new file mode 100644 index 0000000..ff19741 Binary files /dev/null and b/assets/dice/d8/br_d8_6.webp differ diff --git a/assets/dice/d8/br_d8_6_bump.webp b/assets/dice/d8/br_d8_6_bump.webp new file mode 100644 index 0000000..3b0fa0b Binary files /dev/null and b/assets/dice/d8/br_d8_6_bump.webp differ diff --git a/assets/dice/d8/br_d8_7.png b/assets/dice/d8/br_d8_7.png new file mode 100644 index 0000000..c880ddf Binary files /dev/null and b/assets/dice/d8/br_d8_7.png differ diff --git a/assets/dice/d8/br_d8_7.webp b/assets/dice/d8/br_d8_7.webp new file mode 100644 index 0000000..5d2dd84 Binary files /dev/null and b/assets/dice/d8/br_d8_7.webp differ diff --git a/assets/dice/d8/br_d8_7_bump.webp b/assets/dice/d8/br_d8_7_bump.webp new file mode 100644 index 0000000..bd4a4cb Binary files /dev/null and b/assets/dice/d8/br_d8_7_bump.webp differ diff --git a/assets/dice/d8/br_d8_8.png b/assets/dice/d8/br_d8_8.png new file mode 100644 index 0000000..4580325 Binary files /dev/null and b/assets/dice/d8/br_d8_8.png differ diff --git a/assets/dice/d8/br_d8_8.webp b/assets/dice/d8/br_d8_8.webp new file mode 100644 index 0000000..b1cecb2 Binary files /dev/null and b/assets/dice/d8/br_d8_8.webp differ diff --git a/assets/dice/d8/br_d8_8_bump.webp b/assets/dice/d8/br_d8_8_bump.webp new file mode 100644 index 0000000..f2acb74 Binary files /dev/null and b/assets/dice/d8/br_d8_8_bump.webp differ diff --git a/assets/icons/ammo/ammo-ap.svg b/assets/icons/ammo/ammo-ap.svg new file mode 100644 index 0000000..921fdb4 --- /dev/null +++ b/assets/icons/ammo/ammo-ap.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/ammo/ammo-pistol.svg b/assets/icons/ammo/ammo-pistol.svg new file mode 100644 index 0000000..ef3d6d0 --- /dev/null +++ b/assets/icons/ammo/ammo-pistol.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/ammo/ammo-rifle.svg b/assets/icons/ammo/ammo-rifle.svg new file mode 100644 index 0000000..b138752 --- /dev/null +++ b/assets/icons/ammo/ammo-rifle.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/assets/icons/ammo/ammo-shotgun.svg b/assets/icons/ammo/ammo-shotgun.svg new file mode 100644 index 0000000..344745f --- /dev/null +++ b/assets/icons/ammo/ammo-shotgun.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/armor/armor-body-blocker.svg b/assets/icons/armor/armor-body-blocker.svg new file mode 100644 index 0000000..630660b --- /dev/null +++ b/assets/icons/armor/armor-body-blocker.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/armor/armor-caf.svg b/assets/icons/armor/armor-caf.svg new file mode 100644 index 0000000..48d4ae8 --- /dev/null +++ b/assets/icons/armor/armor-caf.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/armor/armor-downtown-jacket.svg b/assets/icons/armor/armor-downtown-jacket.svg new file mode 100644 index 0000000..9bb8054 --- /dev/null +++ b/assets/icons/armor/armor-downtown-jacket.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/assets/icons/armor/armor-ebb-deathsuit.svg b/assets/icons/armor/armor-ebb-deathsuit.svg new file mode 100644 index 0000000..157bbec --- /dev/null +++ b/assets/icons/armor/armor-ebb-deathsuit.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/armor/armor-exo-rig.svg b/assets/icons/armor/armor-exo-rig.svg new file mode 100644 index 0000000..03178d3 --- /dev/null +++ b/assets/icons/armor/armor-exo-rig.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/armor/armor-flak-vest.svg b/assets/icons/armor/armor-flak-vest.svg new file mode 100644 index 0000000..07c8084 --- /dev/null +++ b/assets/icons/armor/armor-flak-vest.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/armor/armor-hard.svg b/assets/icons/armor/armor-hard.svg new file mode 100644 index 0000000..eae5aea --- /dev/null +++ b/assets/icons/armor/armor-hard.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/armor/armor-power.svg b/assets/icons/armor/armor-power.svg new file mode 100644 index 0000000..8170de7 --- /dev/null +++ b/assets/icons/armor/armor-power.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/armor/armor-scout.svg b/assets/icons/armor/armor-scout.svg new file mode 100644 index 0000000..284a271 --- /dev/null +++ b/assets/icons/armor/armor-scout.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/armor/armor-stealth-suit.svg b/assets/icons/armor/armor-stealth-suit.svg new file mode 100644 index 0000000..9210bc2 --- /dev/null +++ b/assets/icons/armor/armor-stealth-suit.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/armor/armor-thresher.svg b/assets/icons/armor/armor-thresher.svg new file mode 100644 index 0000000..6d133a3 --- /dev/null +++ b/assets/icons/armor/armor-thresher.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/blade-runner-failure.png b/assets/icons/blade-runner-failure.png new file mode 100644 index 0000000..a240fb6 Binary files /dev/null and b/assets/icons/blade-runner-failure.png differ diff --git a/assets/icons/blade-runner-failure.svg b/assets/icons/blade-runner-failure.svg new file mode 100644 index 0000000..4637331 --- /dev/null +++ b/assets/icons/blade-runner-failure.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + diff --git a/assets/icons/blade-runner-failure.webp b/assets/icons/blade-runner-failure.webp new file mode 100644 index 0000000..7c2f914 Binary files /dev/null and b/assets/icons/blade-runner-failure.webp differ diff --git a/assets/icons/blade-runner-success.png b/assets/icons/blade-runner-success.png new file mode 100644 index 0000000..8b76713 Binary files /dev/null and b/assets/icons/blade-runner-success.png differ diff --git a/assets/icons/blade-runner-success.svg b/assets/icons/blade-runner-success.svg new file mode 100644 index 0000000..b59d976 --- /dev/null +++ b/assets/icons/blade-runner-success.svg @@ -0,0 +1,31 @@ + + + + + + + + + diff --git a/assets/icons/blade-runner-success.webp b/assets/icons/blade-runner-success.webp new file mode 100644 index 0000000..ce2d0ec Binary files /dev/null and b/assets/icons/blade-runner-success.webp differ diff --git a/assets/icons/cardboard-box-closed.svg b/assets/icons/cardboard-box-closed.svg new file mode 100644 index 0000000..f5cc354 --- /dev/null +++ b/assets/icons/cardboard-box-closed.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/d10-grey.svg b/assets/icons/d10-grey.svg new file mode 100644 index 0000000..0777f00 --- /dev/null +++ b/assets/icons/d10-grey.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/d12-grey.svg b/assets/icons/d12-grey.svg new file mode 100644 index 0000000..5e3c752 --- /dev/null +++ b/assets/icons/d12-grey.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/d6-grey.svg b/assets/icons/d6-grey.svg new file mode 100644 index 0000000..c08174e --- /dev/null +++ b/assets/icons/d6-grey.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/d8-grey.svg b/assets/icons/d8-grey.svg new file mode 100644 index 0000000..eee6de4 --- /dev/null +++ b/assets/icons/d8-grey.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/drugs/drug-blaze-uv.svg b/assets/icons/drugs/drug-blaze-uv.svg new file mode 100644 index 0000000..ffe547a --- /dev/null +++ b/assets/icons/drugs/drug-blaze-uv.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/assets/icons/drugs/drug-bozerker.svg b/assets/icons/drugs/drug-bozerker.svg new file mode 100644 index 0000000..0ecee06 --- /dev/null +++ b/assets/icons/drugs/drug-bozerker.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/drugs/drug-bubbles.svg b/assets/icons/drugs/drug-bubbles.svg new file mode 100644 index 0000000..63724d8 --- /dev/null +++ b/assets/icons/drugs/drug-bubbles.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/assets/icons/drugs/drug-calm-line.svg b/assets/icons/drugs/drug-calm-line.svg new file mode 100644 index 0000000..f9aa602 --- /dev/null +++ b/assets/icons/drugs/drug-calm-line.svg @@ -0,0 +1,8 @@ + + + + + + + diff --git a/assets/icons/drugs/drug-drum.svg b/assets/icons/drugs/drug-drum.svg new file mode 100644 index 0000000..b28477d --- /dev/null +++ b/assets/icons/drugs/drug-drum.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/drugs/drug-flip.svg b/assets/icons/drugs/drug-flip.svg new file mode 100644 index 0000000..72176f6 --- /dev/null +++ b/assets/icons/drugs/drug-flip.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/assets/icons/drugs/drug-honesty.svg b/assets/icons/drugs/drug-honesty.svg new file mode 100644 index 0000000..1c227f2 --- /dev/null +++ b/assets/icons/drugs/drug-honesty.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/drugs/drug-painkillers.svg b/assets/icons/drugs/drug-painkillers.svg new file mode 100644 index 0000000..cbed77b --- /dev/null +++ b/assets/icons/drugs/drug-painkillers.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/assets/icons/drugs/drug-push.svg b/assets/icons/drugs/drug-push.svg new file mode 100644 index 0000000..c657796 --- /dev/null +++ b/assets/icons/drugs/drug-push.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/assets/icons/drugs/drug-redline.svg b/assets/icons/drugs/drug-redline.svg new file mode 100644 index 0000000..3b789ea --- /dev/null +++ b/assets/icons/drugs/drug-redline.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/icons/drugs/drug-rush.svg b/assets/icons/drugs/drug-rush.svg new file mode 100644 index 0000000..2d0da60 --- /dev/null +++ b/assets/icons/drugs/drug-rush.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/drugs/drug-sedative.svg b/assets/icons/drugs/drug-sedative.svg new file mode 100644 index 0000000..cee7380 --- /dev/null +++ b/assets/icons/drugs/drug-sedative.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/assets/icons/drugs/drug-shatter.svg b/assets/icons/drugs/drug-shatter.svg new file mode 100644 index 0000000..b0b2474 --- /dev/null +++ b/assets/icons/drugs/drug-shatter.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/assets/icons/drugs/drug-slosh.svg b/assets/icons/drugs/drug-slosh.svg new file mode 100644 index 0000000..58ae034 --- /dev/null +++ b/assets/icons/drugs/drug-slosh.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/assets/icons/drugs/drug-stimulant.svg b/assets/icons/drugs/drug-stimulant.svg new file mode 100644 index 0000000..d028e32 --- /dev/null +++ b/assets/icons/drugs/drug-stimulant.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/assets/icons/drugs/drug-uv.svg b/assets/icons/drugs/drug-uv.svg new file mode 100644 index 0000000..27be010 --- /dev/null +++ b/assets/icons/drugs/drug-uv.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/ebb/ebb-awareness.svg b/assets/icons/ebb/ebb-awareness.svg new file mode 100644 index 0000000..afd2944 --- /dev/null +++ b/assets/icons/ebb/ebb-awareness.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/assets/icons/ebb/ebb-blast.svg b/assets/icons/ebb/ebb-blast.svg new file mode 100644 index 0000000..7fbc0d5 --- /dev/null +++ b/assets/icons/ebb/ebb-blast.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/ebb/ebb-communicate.svg b/assets/icons/ebb/ebb-communicate.svg new file mode 100644 index 0000000..b0a61f0 --- /dev/null +++ b/assets/icons/ebb/ebb-communicate.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/icons/ebb/ebb-enhance.svg b/assets/icons/ebb/ebb-enhance.svg new file mode 100644 index 0000000..d4500d5 --- /dev/null +++ b/assets/icons/ebb/ebb-enhance.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/ebb/ebb-heal.svg b/assets/icons/ebb/ebb-heal.svg new file mode 100644 index 0000000..1640581 --- /dev/null +++ b/assets/icons/ebb/ebb-heal.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/ebb/ebb-protect.svg b/assets/icons/ebb/ebb-protect.svg new file mode 100644 index 0000000..d86b359 --- /dev/null +++ b/assets/icons/ebb/ebb-protect.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icons/ebb/ebb-realityfold.svg b/assets/icons/ebb/ebb-realityfold.svg new file mode 100644 index 0000000..158fa8b --- /dev/null +++ b/assets/icons/ebb/ebb-realityfold.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/assets/icons/ebb/ebb-senses.svg b/assets/icons/ebb/ebb-senses.svg new file mode 100644 index 0000000..5a193b7 --- /dev/null +++ b/assets/icons/ebb/ebb-senses.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/assets/icons/ebb/ebb-telekinesis.svg b/assets/icons/ebb/ebb-telekinesis.svg new file mode 100644 index 0000000..c7a9dd7 --- /dev/null +++ b/assets/icons/ebb/ebb-telekinesis.svg @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/assets/icons/ebb/ebb-thermal.svg b/assets/icons/ebb/ebb-thermal.svg new file mode 100644 index 0000000..5a6aaa1 --- /dev/null +++ b/assets/icons/ebb/ebb-thermal.svg @@ -0,0 +1,22 @@ + + + + + + diff --git a/assets/icons/equipment/equip-comms.svg b/assets/icons/equipment/equip-comms.svg new file mode 100644 index 0000000..911be53 --- /dev/null +++ b/assets/icons/equipment/equip-comms.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/equipment/equip-cutting-charge.svg b/assets/icons/equipment/equip-cutting-charge.svg new file mode 100644 index 0000000..927ceb6 --- /dev/null +++ b/assets/icons/equipment/equip-cutting-charge.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/equipment/equip-drone.svg b/assets/icons/equipment/equip-drone.svg new file mode 100644 index 0000000..7078ccd --- /dev/null +++ b/assets/icons/equipment/equip-drone.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/equipment/equip-medkit.svg b/assets/icons/equipment/equip-medkit.svg new file mode 100644 index 0000000..984263f --- /dev/null +++ b/assets/icons/equipment/equip-medkit.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/assets/icons/equipment/equip-stim-patch.svg b/assets/icons/equipment/equip-stim-patch.svg new file mode 100644 index 0000000..d04f06b --- /dev/null +++ b/assets/icons/equipment/equip-stim-patch.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/assets/icons/equipment/equip-toolkit.svg b/assets/icons/equipment/equip-toolkit.svg new file mode 100644 index 0000000..85144ad --- /dev/null +++ b/assets/icons/equipment/equip-toolkit.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/assets/icons/specialties/specialty-code-of-honor.svg b/assets/icons/specialties/specialty-code-of-honor.svg new file mode 100644 index 0000000..fd3c689 --- /dev/null +++ b/assets/icons/specialties/specialty-code-of-honor.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/assets/icons/specialties/specialty-cold-adapted.svg b/assets/icons/specialties/specialty-cold-adapted.svg new file mode 100644 index 0000000..83fb616 --- /dev/null +++ b/assets/icons/specialties/specialty-cold-adapted.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/assets/icons/specialties/specialty-combat-drug-user.svg b/assets/icons/specialties/specialty-combat-drug-user.svg new file mode 100644 index 0000000..f2ab7c4 --- /dev/null +++ b/assets/icons/specialties/specialty-combat-drug-user.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/assets/icons/specialties/specialty-compassion.svg b/assets/icons/specialties/specialty-compassion.svg new file mode 100644 index 0000000..6f9a223 --- /dev/null +++ b/assets/icons/specialties/specialty-compassion.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/specialties/specialty-ebb-fury.svg b/assets/icons/specialties/specialty-ebb-fury.svg new file mode 100644 index 0000000..1a98400 --- /dev/null +++ b/assets/icons/specialties/specialty-ebb-fury.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/assets/icons/specialties/specialty-ebb-sensitive.svg b/assets/icons/specialties/specialty-ebb-sensitive.svg new file mode 100644 index 0000000..e9b6bea --- /dev/null +++ b/assets/icons/specialties/specialty-ebb-sensitive.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/icons/specialties/specialty-fast-reflexes.svg b/assets/icons/specialties/specialty-fast-reflexes.svg new file mode 100644 index 0000000..d507b4c --- /dev/null +++ b/assets/icons/specialties/specialty-fast-reflexes.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/specialties/specialty-field-surgeon.svg b/assets/icons/specialties/specialty-field-surgeon.svg new file mode 100644 index 0000000..3891ce9 --- /dev/null +++ b/assets/icons/specialties/specialty-field-surgeon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/icons/specialties/specialty-gut-feeling.svg b/assets/icons/specialties/specialty-gut-feeling.svg new file mode 100644 index 0000000..f460763 --- /dev/null +++ b/assets/icons/specialties/specialty-gut-feeling.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/assets/icons/specialties/specialty-hard-hitter.svg b/assets/icons/specialties/specialty-hard-hitter.svg new file mode 100644 index 0000000..c201f26 --- /dev/null +++ b/assets/icons/specialties/specialty-hard-hitter.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/specialties/specialty-healer.svg b/assets/icons/specialties/specialty-healer.svg new file mode 100644 index 0000000..7161f14 --- /dev/null +++ b/assets/icons/specialties/specialty-healer.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/assets/icons/specialties/specialty-inquisitive.svg b/assets/icons/specialties/specialty-inquisitive.svg new file mode 100644 index 0000000..0620511 --- /dev/null +++ b/assets/icons/specialties/specialty-inquisitive.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/assets/icons/specialties/specialty-menacing.svg b/assets/icons/specialties/specialty-menacing.svg new file mode 100644 index 0000000..60ab60d --- /dev/null +++ b/assets/icons/specialties/specialty-menacing.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/assets/icons/specialties/specialty-regeneration.svg b/assets/icons/specialties/specialty-regeneration.svg new file mode 100644 index 0000000..923066c --- /dev/null +++ b/assets/icons/specialties/specialty-regeneration.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/assets/icons/specialties/specialty-sniper.svg b/assets/icons/specialties/specialty-sniper.svg new file mode 100644 index 0000000..4f56dfa --- /dev/null +++ b/assets/icons/specialties/specialty-sniper.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/assets/icons/specialties/specialty-true-grit.svg b/assets/icons/specialties/specialty-true-grit.svg new file mode 100644 index 0000000..60b9bc5 --- /dev/null +++ b/assets/icons/specialties/specialty-true-grit.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/icons/steering-wheel.svg b/assets/icons/steering-wheel.svg new file mode 100644 index 0000000..27b7056 --- /dev/null +++ b/assets/icons/steering-wheel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/weapons/weapon-auto-shotgun.svg b/assets/icons/weapons/weapon-auto-shotgun.svg new file mode 100644 index 0000000..1ecedfa --- /dev/null +++ b/assets/icons/weapons/weapon-auto-shotgun.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/weapons/weapon-baton.svg b/assets/icons/weapons/weapon-baton.svg new file mode 100644 index 0000000..65a2e6b --- /dev/null +++ b/assets/icons/weapons/weapon-baton.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/weapons/weapon-bla-046m.svg b/assets/icons/weapons/weapon-bla-046m.svg new file mode 100644 index 0000000..550a8ae --- /dev/null +++ b/assets/icons/weapons/weapon-bla-046m.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/weapons/weapon-caf-smg.svg b/assets/icons/weapons/weapon-caf-smg.svg new file mode 100644 index 0000000..db9031f --- /dev/null +++ b/assets/icons/weapons/weapon-caf-smg.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/weapons/weapon-chainaxe.svg b/assets/icons/weapons/weapon-chainaxe.svg new file mode 100644 index 0000000..6f7201b --- /dev/null +++ b/assets/icons/weapons/weapon-chainaxe.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/weapons/weapon-chainsaw.svg b/assets/icons/weapons/weapon-chainsaw.svg new file mode 100644 index 0000000..3d20f65 --- /dev/null +++ b/assets/icons/weapons/weapon-chainsaw.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/weapons/weapon-claws.svg b/assets/icons/weapons/weapon-claws.svg new file mode 100644 index 0000000..23ed260 --- /dev/null +++ b/assets/icons/weapons/weapon-claws.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/weapons/weapon-ebb-sword.svg b/assets/icons/weapons/weapon-ebb-sword.svg new file mode 100644 index 0000000..da26178 --- /dev/null +++ b/assets/icons/weapons/weapon-ebb-sword.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/weapons/weapon-fen-204.svg b/assets/icons/weapons/weapon-fen-204.svg new file mode 100644 index 0000000..1594738 --- /dev/null +++ b/assets/icons/weapons/weapon-fen-204.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/weapons/weapon-fen-30-30.svg b/assets/icons/weapons/weapon-fen-30-30.svg new file mode 100644 index 0000000..3274db5 --- /dev/null +++ b/assets/icons/weapons/weapon-fen-30-30.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/weapons/weapon-fen-603.svg b/assets/icons/weapons/weapon-fen-603.svg new file mode 100644 index 0000000..4c632c9 --- /dev/null +++ b/assets/icons/weapons/weapon-fen-603.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/weapons/weapon-fen-706.svg b/assets/icons/weapons/weapon-fen-706.svg new file mode 100644 index 0000000..5077816 --- /dev/null +++ b/assets/icons/weapons/weapon-fen-706.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/weapons/weapon-fen-ar.svg b/assets/icons/weapons/weapon-fen-ar.svg new file mode 100644 index 0000000..c114d89 --- /dev/null +++ b/assets/icons/weapons/weapon-fen-ar.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/weapons/weapon-ga-9442.svg b/assets/icons/weapons/weapon-ga-9442.svg new file mode 100644 index 0000000..1690ea8 --- /dev/null +++ b/assets/icons/weapons/weapon-ga-9442.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/weapons/weapon-ga-9443.svg b/assets/icons/weapons/weapon-ga-9443.svg new file mode 100644 index 0000000..41fc51d --- /dev/null +++ b/assets/icons/weapons/weapon-ga-9443.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/weapons/weapon-gash-fist.svg b/assets/icons/weapons/weapon-gash-fist.svg new file mode 100644 index 0000000..2a78dc1 --- /dev/null +++ b/assets/icons/weapons/weapon-gash-fist.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/weapons/weapon-grenade-launcher.svg b/assets/icons/weapons/weapon-grenade-launcher.svg new file mode 100644 index 0000000..c446345 --- /dev/null +++ b/assets/icons/weapons/weapon-grenade-launcher.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/weapons/weapon-grenade.svg b/assets/icons/weapons/weapon-grenade.svg new file mode 100644 index 0000000..dc8e233 --- /dev/null +++ b/assets/icons/weapons/weapon-grenade.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/weapons/weapon-heavy-autocannon.svg b/assets/icons/weapons/weapon-heavy-autocannon.svg new file mode 100644 index 0000000..89a2705 --- /dev/null +++ b/assets/icons/weapons/weapon-heavy-autocannon.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/weapons/weapon-knife.svg b/assets/icons/weapons/weapon-knife.svg new file mode 100644 index 0000000..42cec8b --- /dev/null +++ b/assets/icons/weapons/weapon-knife.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/assets/icons/weapons/weapon-kps-shotgun.svg b/assets/icons/weapons/weapon-kps-shotgun.svg new file mode 100644 index 0000000..39c5f46 --- /dev/null +++ b/assets/icons/weapons/weapon-kps-shotgun.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/weapons/weapon-lmg.svg b/assets/icons/weapons/weapon-lmg.svg new file mode 100644 index 0000000..4dbaf7c --- /dev/null +++ b/assets/icons/weapons/weapon-lmg.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/weapons/weapon-mac-knife.svg b/assets/icons/weapons/weapon-mac-knife.svg new file mode 100644 index 0000000..bf72285 --- /dev/null +++ b/assets/icons/weapons/weapon-mac-knife.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/assets/icons/weapons/weapon-mono-blade.svg b/assets/icons/weapons/weapon-mono-blade.svg new file mode 100644 index 0000000..6bb1c79 --- /dev/null +++ b/assets/icons/weapons/weapon-mono-blade.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/assets/icons/weapons/weapon-mutilator-sword.svg b/assets/icons/weapons/weapon-mutilator-sword.svg new file mode 100644 index 0000000..81db337 --- /dev/null +++ b/assets/icons/weapons/weapon-mutilator-sword.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/weapons/weapon-pacifier-baton.svg b/assets/icons/weapons/weapon-pacifier-baton.svg new file mode 100644 index 0000000..f83a607 --- /dev/null +++ b/assets/icons/weapons/weapon-pacifier-baton.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/weapons/weapon-pistol.svg b/assets/icons/weapons/weapon-pistol.svg new file mode 100644 index 0000000..8e833dc --- /dev/null +++ b/assets/icons/weapons/weapon-pistol.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/weapons/weapon-power-blade.svg b/assets/icons/weapons/weapon-power-blade.svg new file mode 100644 index 0000000..fcd1dbf --- /dev/null +++ b/assets/icons/weapons/weapon-power-blade.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/assets/icons/weapons/weapon-power-claymore.svg b/assets/icons/weapons/weapon-power-claymore.svg new file mode 100644 index 0000000..ea5b48f --- /dev/null +++ b/assets/icons/weapons/weapon-power-claymore.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/weapons/weapon-rifle.svg b/assets/icons/weapons/weapon-rifle.svg new file mode 100644 index 0000000..b91b416 --- /dev/null +++ b/assets/icons/weapons/weapon-rifle.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/weapons/weapon-riot-shield.svg b/assets/icons/weapons/weapon-riot-shield.svg new file mode 100644 index 0000000..ca8f6c5 --- /dev/null +++ b/assets/icons/weapons/weapon-riot-shield.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/assets/icons/weapons/weapon-shaktar-claws.svg b/assets/icons/weapons/weapon-shaktar-claws.svg new file mode 100644 index 0000000..034765e --- /dev/null +++ b/assets/icons/weapons/weapon-shaktar-claws.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/assets/icons/weapons/weapon-sla-blade.svg b/assets/icons/weapons/weapon-sla-blade.svg new file mode 100644 index 0000000..8cd87b1 --- /dev/null +++ b/assets/icons/weapons/weapon-sla-blade.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/weapons/weapon-smg.svg b/assets/icons/weapons/weapon-smg.svg new file mode 100644 index 0000000..acffa7c --- /dev/null +++ b/assets/icons/weapons/weapon-smg.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/weapons/weapon-sniper-rifle.svg b/assets/icons/weapons/weapon-sniper-rifle.svg new file mode 100644 index 0000000..bdb2a29 --- /dev/null +++ b/assets/icons/weapons/weapon-sniper-rifle.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/weapons/weapon-stormer-claws.svg b/assets/icons/weapons/weapon-stormer-claws.svg new file mode 100644 index 0000000..70ff0a0 --- /dev/null +++ b/assets/icons/weapons/weapon-stormer-claws.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/assets/icons/weapons/weapon-stormer-teeth.svg b/assets/icons/weapons/weapon-stormer-teeth.svg new file mode 100644 index 0000000..feb39b3 --- /dev/null +++ b/assets/icons/weapons/weapon-stormer-teeth.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/weapons/weapon-stun-grenade.svg b/assets/icons/weapons/weapon-stun-grenade.svg new file mode 100644 index 0000000..44a52de --- /dev/null +++ b/assets/icons/weapons/weapon-stun-grenade.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/weapons/weapon-vevaphon-claws.svg b/assets/icons/weapons/weapon-vevaphon-claws.svg new file mode 100644 index 0000000..8d7b2e3 --- /dev/null +++ b/assets/icons/weapons/weapon-vevaphon-claws.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/assets/icons/weapons/weapon-vibro-sabre.svg b/assets/icons/weapons/weapon-vibro-sabre.svg new file mode 100644 index 0000000..d238cf3 --- /dev/null +++ b/assets/icons/weapons/weapon-vibro-sabre.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/assets/logos/BSL-D6_bannerlogo_H150.png b/assets/logos/BSL-D6_bannerlogo_H150.png new file mode 100644 index 0000000..79a186c Binary files /dev/null and b/assets/logos/BSL-D6_bannerlogo_H150.png differ diff --git a/assets/logos/BSL-D6_icon.webp b/assets/logos/BSL-D6_icon.webp new file mode 100644 index 0000000..2e17dce Binary files /dev/null and b/assets/logos/BSL-D6_icon.webp differ diff --git a/assets/logos/BSL-D6_logo_W200.png b/assets/logos/BSL-D6_logo_W200.png new file mode 100644 index 0000000..937d254 Binary files /dev/null and b/assets/logos/BSL-D6_logo_W200.png differ diff --git a/assets/logos/FL_eng_cent_neg.webp b/assets/logos/FL_eng_cent_neg.webp new file mode 100644 index 0000000..dc52536 Binary files /dev/null and b/assets/logos/FL_eng_cent_neg.webp differ diff --git a/assets/logos/frialigan-wide-bold-logo.png b/assets/logos/frialigan-wide-bold-logo.png new file mode 100644 index 0000000..1cd6727 Binary files /dev/null and b/assets/logos/frialigan-wide-bold-logo.png differ diff --git a/assets/logos/fvtt-fria-ligan-logo2.png b/assets/logos/fvtt-fria-ligan-logo2.png new file mode 100644 index 0000000..d9ca766 Binary files /dev/null and b/assets/logos/fvtt-fria-ligan-logo2.png differ diff --git a/assets/logos/hod-publishing.webp b/assets/logos/hod-publishing.webp new file mode 100644 index 0000000..ad6c61d Binary files /dev/null and b/assets/logos/hod-publishing.webp differ diff --git a/assets/logos/lapd_br_badge.webp b/assets/logos/lapd_br_badge.webp new file mode 100644 index 0000000..63e3333 Binary files /dev/null and b/assets/logos/lapd_br_badge.webp differ diff --git a/assets/messages/messages.jsonc b/assets/messages/messages.jsonc new file mode 100644 index 0000000..7853fec --- /dev/null +++ b/assets/messages/messages.jsonc @@ -0,0 +1,82 @@ +[ + { + // "min-core-version": "9.0.0", //optional, inclusive + // "max-core-version": "10.0.0", //optional, exclusive + "min-sys-version": "0.0.0", //optional, inclusive + // "max-sys-version": "11.0.0", //optional, exclusive + "type": "chat", // Type of message: [prompt, chat] + "display": "once", // Time until it goes away: [once, infinite, mm.dd.yyyy] + "title": "Welcome to The Blade Runner RPG", + "content": "

Hi, {name}!

Blade Runner is a neon-noir wonderland that’ll take your breath away. One way or another. An evocative world of conflicts and contrasts that dares to ask the hard questions and investigate the powers of empathy, the poisons of fear, and the burdens of being human during inhumane times. An iconic and unforgiving playground of endless possibilities that picks you up, slaps you in the face, and tells you to wake up. Time to live. Or time to die.

To learn how to use this game system, you can read the manual:

" + }, + { + // "min-core-version": "9.0.0", //optional, inclusive + // "max-core-version": "10.0.0", //optional, exclusive + "min-sys-version": "10.0.1", //optional, inclusive + "max-sys-version": "10.1.0", //optional, exclusive + "type": "chat", // Type of message: [prompt, chat] + "display": "once", // Time until it goes away: [once, infinite, mm.dd.yyyy] + "title": "Patch 10.0.1 (2022-11-29)", + "content": "

Added

  • French translation

Fixed

  • Missing pause logo
  • Other small fixes
" + }, + { + // "min-core-version": "9.0.0", //optional, inclusive + // "max-core-version": "10.0.0", //optional, exclusive + "min-sys-version": "10.0.2", //optional, inclusive + "max-sys-version": "10.1.0", //optional, exclusive + "type": "chat", // Type of message: [prompt, chat] + "display": "once", // Time until it goes away: [once, infinite, mm.dd.yyyy] + "title": "Patch 10.0.2 (2022-12-05)", + "content": "

Added

  • Dice choice when pushing
  • Health & Resolve option for modifiers in the item sheet (the character's capacities are automatically updated accordingly)

Fixed

  • Display the \"roll item\" button in the sheet header only when needed
  • An error when rolling two dice of the same size with disadvantage
  • Some French translations being too long
  • Some text color styles
" + }, + { + // "min-core-version": "9.0.0", //optional, inclusive + // "max-core-version": "10.0.0", //optional, exclusive + "min-sys-version": "10.1.0", //optional, inclusive + "max-sys-version": "12.0.0", //optional, exclusive + "type": "chat", // Type of message: [prompt, chat] + "display": "once", // Time until it goes away: [once, infinite, mm.dd.yyyy] + "title": "Update 10.1.0 (2023-06-26)", + "content": "

Added

  • Vehicle sheet
  • Items pile sheet
  • Draw critical injuries from the roll chat card
  • Support for Active Effects
  • New game system settings
  • New translations: German, Korean and Spanish

Changed

  • Better support for custom actor actions
  • Minor UI improvements

Fixed

  • Issue with default initiative
  • Style issue with Minimal UI module
  • Minor bug fixes
" + }, + { + // "min-core-version": "9.0.0", //optional, inclusive + // "max-core-version": "10.0.0", //optional, exclusive + "min-sys-version": "11.0.0", //optional, inclusive + "max-sys-version": "11.1.0", //optional, exclusive + "type": "chat", // Type of message: [prompt, chat] + "display": "once", // Time until it goes away: [once, infinite, mm.dd.yyyy] + "title": "Update 11.0.0 (2023-06-26)", + "content": "

Added

  • Compatibility with Foundry V11
" + }, + { + // "min-core-version": "9.0.0", //optional, inclusive + // "max-core-version": "10.0.0", //optional, exclusive + "min-sys-version": "11.0.1", //optional, inclusive + "max-sys-version": "11.1.0", //optional, exclusive + "type": "chat", // Type of message: [prompt, chat] + "display": "once", // Time until it goes away: [once, infinite, mm.dd.yyyy] + "title": "Update 11.0.1 (2023-07-18)", + "content": "

Fixed

  • Enabled German, Spanish and Korean languages (a parameter was missing).
  • Fixed translations issues in French and German.
" + }, + { + // "min-core-version": "9.0.0", //optional, inclusive + // "max-core-version": "10.0.0", //optional, exclusive + "min-sys-version": "11.0.2", //optional, inclusive + "max-sys-version": "11.1.0", //optional, exclusive + "type": "chat", // Type of message: [prompt, chat] + "display": "once", // Time until it goes away: [once, infinite, mm.dd.yyyy] + "title": "Update 11.0.2 (2024-03-27)", + "content": "

Added

  • Added support for the upcoming Fiery Angels module.
  • Added new setting to disable handwritten font.
" + }, + { + // "min-core-version": "9.0.0", //optional, inclusive + // "max-core-version": "10.0.0", //optional, exclusive + "min-sys-version": "13.0.0", //optional, inclusive + "max-sys-version": "13.1.0", //optional, exclusive + "type": "chat", // Type of message: [prompt, chat] + "display": "once", // Time until it goes away: [once, infinite, mm.dd.yyyy] + "title": "Update 13.0.0 (2025-06-11)", + "content": "
  • Added compatibility with Foundry V13.
  • Added support for Item Piles.

See the changelog for the complete list of changes.

" + } +] diff --git a/assets/npc-art/abilities/attack.png b/assets/npc-art/abilities/attack.png new file mode 100644 index 0000000..a2b339a Binary files /dev/null and b/assets/npc-art/abilities/attack.png differ diff --git a/assets/npc-art/abilities/contagion.png b/assets/npc-art/abilities/contagion.png new file mode 100644 index 0000000..3c47ef7 Binary files /dev/null and b/assets/npc-art/abilities/contagion.png differ diff --git a/assets/npc-art/abilities/enemy.png b/assets/npc-art/abilities/enemy.png new file mode 100644 index 0000000..b989b47 Binary files /dev/null and b/assets/npc-art/abilities/enemy.png differ diff --git a/assets/npc-art/abilities/fast-reflexes.png b/assets/npc-art/abilities/fast-reflexes.png new file mode 100644 index 0000000..0055459 Binary files /dev/null and b/assets/npc-art/abilities/fast-reflexes.png differ diff --git a/assets/npc-art/abilities/hard-hitting.png b/assets/npc-art/abilities/hard-hitting.png new file mode 100644 index 0000000..5bb7b4c Binary files /dev/null and b/assets/npc-art/abilities/hard-hitting.png differ diff --git a/assets/npc-art/abilities/menacing.png b/assets/npc-art/abilities/menacing.png new file mode 100644 index 0000000..6094520 Binary files /dev/null and b/assets/npc-art/abilities/menacing.png differ diff --git a/assets/npc-art/abilities/psychosis.png b/assets/npc-art/abilities/psychosis.png new file mode 100644 index 0000000..9acb50a Binary files /dev/null and b/assets/npc-art/abilities/psychosis.png differ diff --git a/assets/npc-art/abilities/regeneration.png b/assets/npc-art/abilities/regeneration.png new file mode 100644 index 0000000..5cbeeaa Binary files /dev/null and b/assets/npc-art/abilities/regeneration.png differ diff --git a/assets/npc-art/armor/combat.png b/assets/npc-art/armor/combat.png new file mode 100644 index 0000000..151d457 Binary files /dev/null and b/assets/npc-art/armor/combat.png differ diff --git a/assets/npc-art/armor/exo.png b/assets/npc-art/armor/exo.png new file mode 100644 index 0000000..fb86703 Binary files /dev/null and b/assets/npc-art/armor/exo.png differ diff --git a/assets/npc-art/armor/heavy.png b/assets/npc-art/armor/heavy.png new file mode 100644 index 0000000..921b9cb Binary files /dev/null and b/assets/npc-art/armor/heavy.png differ diff --git a/assets/npc-art/armor/light.png b/assets/npc-art/armor/light.png new file mode 100644 index 0000000..7f38ef6 Binary files /dev/null and b/assets/npc-art/armor/light.png differ diff --git a/assets/npc-art/armor/medium.png b/assets/npc-art/armor/medium.png new file mode 100644 index 0000000..ed4007e Binary files /dev/null and b/assets/npc-art/armor/medium.png differ diff --git a/assets/npc-art/armor/street.png b/assets/npc-art/armor/street.png new file mode 100644 index 0000000..35ee14c Binary files /dev/null and b/assets/npc-art/armor/street.png differ diff --git a/assets/npc-art/portraits/carrion.png b/assets/npc-art/portraits/carrion.png new file mode 100644 index 0000000..c48da90 Binary files /dev/null and b/assets/npc-art/portraits/carrion.png differ diff --git a/assets/npc-art/portraits/enemy-generic.png b/assets/npc-art/portraits/enemy-generic.png new file mode 100644 index 0000000..b989b47 Binary files /dev/null and b/assets/npc-art/portraits/enemy-generic.png differ diff --git a/assets/npc-art/portraits/machine.png b/assets/npc-art/portraits/machine.png new file mode 100644 index 0000000..41746fd Binary files /dev/null and b/assets/npc-art/portraits/machine.png differ diff --git a/assets/npc-art/portraits/monstaret.png b/assets/npc-art/portraits/monstaret.png new file mode 100644 index 0000000..425b979 Binary files /dev/null and b/assets/npc-art/portraits/monstaret.png differ diff --git a/assets/npc-art/portraits/serialkiller.png b/assets/npc-art/portraits/serialkiller.png new file mode 100644 index 0000000..70cb6c4 Binary files /dev/null and b/assets/npc-art/portraits/serialkiller.png differ diff --git a/assets/npc-art/portraits/streetgang.png b/assets/npc-art/portraits/streetgang.png new file mode 100644 index 0000000..40257d1 Binary files /dev/null and b/assets/npc-art/portraits/streetgang.png differ diff --git a/assets/npc-art/weapons/baton.png b/assets/npc-art/weapons/baton.png new file mode 100644 index 0000000..4550e99 Binary files /dev/null and b/assets/npc-art/weapons/baton.png differ diff --git a/assets/npc-art/weapons/blade.png b/assets/npc-art/weapons/blade.png new file mode 100644 index 0000000..1e2ad10 Binary files /dev/null and b/assets/npc-art/weapons/blade.png differ diff --git a/assets/npc-art/weapons/chainsaw.png b/assets/npc-art/weapons/chainsaw.png new file mode 100644 index 0000000..a475ace Binary files /dev/null and b/assets/npc-art/weapons/chainsaw.png differ diff --git a/assets/npc-art/weapons/claws.png b/assets/npc-art/weapons/claws.png new file mode 100644 index 0000000..2066b51 Binary files /dev/null and b/assets/npc-art/weapons/claws.png differ diff --git a/assets/npc-art/weapons/club.png b/assets/npc-art/weapons/club.png new file mode 100644 index 0000000..4c1ce1a Binary files /dev/null and b/assets/npc-art/weapons/club.png differ diff --git a/assets/npc-art/weapons/combat-knife.png b/assets/npc-art/weapons/combat-knife.png new file mode 100644 index 0000000..07f8404 Binary files /dev/null and b/assets/npc-art/weapons/combat-knife.png differ diff --git a/assets/npc-art/weapons/hatchet.png b/assets/npc-art/weapons/hatchet.png new file mode 100644 index 0000000..73d97b6 Binary files /dev/null and b/assets/npc-art/weapons/hatchet.png differ diff --git a/assets/npc-art/weapons/heavy.png b/assets/npc-art/weapons/heavy.png new file mode 100644 index 0000000..1afaef9 Binary files /dev/null and b/assets/npc-art/weapons/heavy.png differ diff --git a/assets/npc-art/weapons/knife.png b/assets/npc-art/weapons/knife.png new file mode 100644 index 0000000..e58d164 Binary files /dev/null and b/assets/npc-art/weapons/knife.png differ diff --git a/assets/npc-art/weapons/machine-pistol.png b/assets/npc-art/weapons/machine-pistol.png new file mode 100644 index 0000000..43fab81 Binary files /dev/null and b/assets/npc-art/weapons/machine-pistol.png differ diff --git a/assets/npc-art/weapons/natural-weapon.png b/assets/npc-art/weapons/natural-weapon.png new file mode 100644 index 0000000..3daa78a Binary files /dev/null and b/assets/npc-art/weapons/natural-weapon.png differ diff --git a/assets/npc-art/weapons/pistol.png b/assets/npc-art/weapons/pistol.png new file mode 100644 index 0000000..c10de64 Binary files /dev/null and b/assets/npc-art/weapons/pistol.png differ diff --git a/assets/npc-art/weapons/rifle.png b/assets/npc-art/weapons/rifle.png new file mode 100644 index 0000000..6dc0a08 Binary files /dev/null and b/assets/npc-art/weapons/rifle.png differ diff --git a/assets/npc-art/weapons/sabre.png b/assets/npc-art/weapons/sabre.png new file mode 100644 index 0000000..e4d2824 Binary files /dev/null and b/assets/npc-art/weapons/sabre.png differ diff --git a/assets/npc-art/weapons/shotgun.png b/assets/npc-art/weapons/shotgun.png new file mode 100644 index 0000000..b40cba1 Binary files /dev/null and b/assets/npc-art/weapons/shotgun.png differ diff --git a/assets/npc-art/weapons/smg.png b/assets/npc-art/weapons/smg.png new file mode 100644 index 0000000..fb82fd3 Binary files /dev/null and b/assets/npc-art/weapons/smg.png differ diff --git a/assets/npc-art/weapons/sniper.png b/assets/npc-art/weapons/sniper.png new file mode 100644 index 0000000..b41de39 Binary files /dev/null and b/assets/npc-art/weapons/sniper.png differ diff --git a/assets/plugins/dice-so-nice/fvtt-dicesonice-SFX-1663792064908.json b/assets/plugins/dice-so-nice/fvtt-dicesonice-SFX-1663792064908.json new file mode 100644 index 0000000..219b247 --- /dev/null +++ b/assets/plugins/dice-so-nice/fvtt-dicesonice-SFX-1663792064908.json @@ -0,0 +1,96 @@ +{ + "sfxList": [ + { + "diceType": "d6", + "onResult": [ + "6" + ], + "specialEffect": "PlayAnimationParticleSpiral", + "options": { + "isGlobal": false + } + }, + { + "diceType": "d8", + "onResult": [ + "6", + "7", + "8" + ], + "specialEffect": "PlayAnimationParticleSpiral", + "options": { + "isGlobal": false + } + }, + { + "diceType": "d10", + "onResult": [ + "6", + "7", + "8", + "9", + "10" + ], + "specialEffect": "PlayAnimationParticleSpiral", + "options": { + "isGlobal": false + } + }, + { + "diceType": "d12", + "onResult": [ + "6", + "7", + "8", + "9", + "10", + "11", + "12" + ], + "specialEffect": "PlayAnimationParticleSpiral", + "options": { + "isGlobal": false + } + }, + { + "diceType": "d6", + "onResult": [ + "1" + ], + "specialEffect": "PlayAnimationImpact", + "options": { + "isGlobal": false + } + }, + { + "diceType": "d8", + "onResult": [ + "1" + ], + "specialEffect": "PlayAnimationImpact", + "options": { + "isGlobal": false + } + }, + { + "diceType": "d10", + "onResult": [ + "1" + ], + "specialEffect": "PlayAnimationImpact", + "options": { + "isGlobal": false + } + }, + { + "diceType": "d12", + "onResult": [ + "1" + ], + "specialEffect": "PlayAnimationImpact", + "options": { + "isGlobal": false + } + } + ] +} \ No newline at end of file diff --git a/assets/racial/battle-claws.svg b/assets/racial/battle-claws.svg new file mode 100644 index 0000000..37d2452 --- /dev/null +++ b/assets/racial/battle-claws.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + WARRIOR + diff --git a/assets/racial/blade-proficiency.svg b/assets/racial/blade-proficiency.svg new file mode 100644 index 0000000..e12a56c --- /dev/null +++ b/assets/racial/blade-proficiency.svg @@ -0,0 +1,11 @@ + + + + + + + + + + BLADES + diff --git a/assets/racial/drug-dependency.svg b/assets/racial/drug-dependency.svg new file mode 100644 index 0000000..f786def --- /dev/null +++ b/assets/racial/drug-dependency.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + DEPEND + diff --git a/assets/racial/natural-weapons.svg b/assets/racial/natural-weapons.svg new file mode 100644 index 0000000..6308dff --- /dev/null +++ b/assets/racial/natural-weapons.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + CLAWS + diff --git a/assets/racial/regen.svg b/assets/racial/regen.svg new file mode 100644 index 0000000..5318e73 --- /dev/null +++ b/assets/racial/regen.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + REGEN + diff --git a/assets/racial/shaper-senses.svg b/assets/racial/shaper-senses.svg new file mode 100644 index 0000000..4fcc84b --- /dev/null +++ b/assets/racial/shaper-senses.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + SHAPER + diff --git a/assets/racial/social-versatility.svg b/assets/racial/social-versatility.svg new file mode 100644 index 0000000..31fc01d --- /dev/null +++ b/assets/racial/social-versatility.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + SOCIAL + diff --git a/assets/racial/warrior-caste.svg b/assets/racial/warrior-caste.svg new file mode 100644 index 0000000..98cc7ff --- /dev/null +++ b/assets/racial/warrior-caste.svg @@ -0,0 +1,6 @@ + + + + + CASTE + diff --git a/assets/screenshots/actor-sheet-archetype.png b/assets/screenshots/actor-sheet-archetype.png new file mode 100644 index 0000000..2677d5a Binary files /dev/null and b/assets/screenshots/actor-sheet-archetype.png differ diff --git a/assets/screenshots/actor-sheet-capacities.png b/assets/screenshots/actor-sheet-capacities.png new file mode 100644 index 0000000..fb32d6c Binary files /dev/null and b/assets/screenshots/actor-sheet-capacities.png differ diff --git a/assets/screenshots/actor-sheet-config.png b/assets/screenshots/actor-sheet-config.png new file mode 100644 index 0000000..40ad07f Binary files /dev/null and b/assets/screenshots/actor-sheet-config.png differ diff --git a/assets/screenshots/actor-sheet-header-buttons.png b/assets/screenshots/actor-sheet-header-buttons.png new file mode 100644 index 0000000..dda7a1b Binary files /dev/null and b/assets/screenshots/actor-sheet-header-buttons.png differ diff --git a/assets/screenshots/actor-sheet-inventory-item-icons.png b/assets/screenshots/actor-sheet-inventory-item-icons.png new file mode 100644 index 0000000..dda30bc Binary files /dev/null and b/assets/screenshots/actor-sheet-inventory-item-icons.png differ diff --git a/assets/screenshots/actor-sheet-tab-combat-actions.png b/assets/screenshots/actor-sheet-tab-combat-actions.png new file mode 100644 index 0000000..4aac7a7 Binary files /dev/null and b/assets/screenshots/actor-sheet-tab-combat-actions.png differ diff --git a/assets/screenshots/actor-sheet-tab-combat-items.png b/assets/screenshots/actor-sheet-tab-combat-items.png new file mode 100644 index 0000000..dc8d3b8 Binary files /dev/null and b/assets/screenshots/actor-sheet-tab-combat-items.png differ diff --git a/assets/screenshots/actor-sheet-tab-inventory.png b/assets/screenshots/actor-sheet-tab-inventory.png new file mode 100644 index 0000000..e450fe4 Binary files /dev/null and b/assets/screenshots/actor-sheet-tab-inventory.png differ diff --git a/assets/screenshots/actor-sheet-tab-mods.png b/assets/screenshots/actor-sheet-tab-mods.png new file mode 100644 index 0000000..b752e61 Binary files /dev/null and b/assets/screenshots/actor-sheet-tab-mods.png differ diff --git a/assets/screenshots/actor-sheet.png b/assets/screenshots/actor-sheet.png new file mode 100644 index 0000000..22ab819 Binary files /dev/null and b/assets/screenshots/actor-sheet.png differ diff --git a/assets/screenshots/item-sheet-crit.png b/assets/screenshots/item-sheet-crit.png new file mode 100644 index 0000000..c574298 Binary files /dev/null and b/assets/screenshots/item-sheet-crit.png differ diff --git a/assets/screenshots/item-sheet-header-buttons.png b/assets/screenshots/item-sheet-header-buttons.png new file mode 100644 index 0000000..c32356a Binary files /dev/null and b/assets/screenshots/item-sheet-header-buttons.png differ diff --git a/assets/screenshots/item-sheet-modifiers.png b/assets/screenshots/item-sheet-modifiers.png new file mode 100644 index 0000000..697c032 Binary files /dev/null and b/assets/screenshots/item-sheet-modifiers.png differ diff --git a/assets/screenshots/item-sheet-weapon-roll.png b/assets/screenshots/item-sheet-weapon-roll.png new file mode 100644 index 0000000..1a5e09d Binary files /dev/null and b/assets/screenshots/item-sheet-weapon-roll.png differ diff --git a/assets/screenshots/item-sheet-weapon.png b/assets/screenshots/item-sheet-weapon.png new file mode 100644 index 0000000..205a1e4 Binary files /dev/null and b/assets/screenshots/item-sheet-weapon.png differ diff --git a/assets/screenshots/roll-apply-damage.png b/assets/screenshots/roll-apply-damage.png new file mode 100644 index 0000000..d48667a Binary files /dev/null and b/assets/screenshots/roll-apply-damage.png differ diff --git a/assets/screenshots/roll-chat-card.png b/assets/screenshots/roll-chat-card.png new file mode 100644 index 0000000..cd6cafb Binary files /dev/null and b/assets/screenshots/roll-chat-card.png differ diff --git a/assets/screenshots/roll-dice-dialog.png b/assets/screenshots/roll-dice-dialog.png new file mode 100644 index 0000000..f00178a Binary files /dev/null and b/assets/screenshots/roll-dice-dialog.png differ diff --git a/assets/screenshots/token-pc.png b/assets/screenshots/token-pc.png new file mode 100644 index 0000000..9a16c5b Binary files /dev/null and b/assets/screenshots/token-pc.png differ diff --git a/assets/screenshots/vehicle-sheet-actions.png b/assets/screenshots/vehicle-sheet-actions.png new file mode 100644 index 0000000..5a0482e Binary files /dev/null and b/assets/screenshots/vehicle-sheet-actions.png differ diff --git a/assets/screenshots/vehicle-sheet-crew.png b/assets/screenshots/vehicle-sheet-crew.png new file mode 100644 index 0000000..f77a929 Binary files /dev/null and b/assets/screenshots/vehicle-sheet-crew.png differ diff --git a/assets/screenshots/vehicle-sheet-hull.png b/assets/screenshots/vehicle-sheet-hull.png new file mode 100644 index 0000000..1d080bd Binary files /dev/null and b/assets/screenshots/vehicle-sheet-hull.png differ diff --git a/assets/screenshots/vehicle-sheet.png b/assets/screenshots/vehicle-sheet.png new file mode 100644 index 0000000..b882ada Binary files /dev/null and b/assets/screenshots/vehicle-sheet.png differ diff --git a/assets/splash.webp b/assets/splash.webp new file mode 100644 index 0000000..e846607 Binary files /dev/null and b/assets/splash.webp differ diff --git a/assets/textures/character-sheet-background.webp b/assets/textures/character-sheet-background.webp new file mode 100644 index 0000000..7aad530 Binary files /dev/null and b/assets/textures/character-sheet-background.webp differ diff --git a/assets/textures/character-sheet-editor.webp b/assets/textures/character-sheet-editor.webp new file mode 100644 index 0000000..2900783 Binary files /dev/null and b/assets/textures/character-sheet-editor.webp differ diff --git a/assets/textures/chat-card-background.webp b/assets/textures/chat-card-background.webp new file mode 100644 index 0000000..6ab12da Binary files /dev/null and b/assets/textures/chat-card-background.webp differ diff --git a/assets/textures/journal-sheet-background.webp b/assets/textures/journal-sheet-background.webp new file mode 100644 index 0000000..90813d7 Binary files /dev/null and b/assets/textures/journal-sheet-background.webp differ diff --git a/assets/textures/page-text-zone-background-blue-lighter.webp b/assets/textures/page-text-zone-background-blue-lighter.webp new file mode 100644 index 0000000..fb34b92 Binary files /dev/null and b/assets/textures/page-text-zone-background-blue-lighter.webp differ diff --git a/assets/textures/page-text-zone-background-blue.webp b/assets/textures/page-text-zone-background-blue.webp new file mode 100644 index 0000000..b47b499 Binary files /dev/null and b/assets/textures/page-text-zone-background-blue.webp differ diff --git a/assets/textures/page-text-zone-background.webp b/assets/textures/page-text-zone-background.webp new file mode 100644 index 0000000..e0d7f57 Binary files /dev/null and b/assets/textures/page-text-zone-background.webp differ diff --git a/build-packs.mjs b/build-packs.mjs new file mode 100644 index 0000000..d2aac93 --- /dev/null +++ b/build-packs.mjs @@ -0,0 +1,126 @@ +/** + * build-packs.mjs + * Converts JSON source files in /packs/ into Foundry v14 LevelDB compendium packs. + * + * Usage: node build-packs.mjs + * + * To add new items: edit the relevant JSON file in /packs/, then re-run this script + * and push to GitHub. Foundry will pick up the new items on next install/update. + */ + +import { ClassicLevel } from 'classic-level'; +import { readFileSync, rmSync, existsSync } from 'fs'; +import { join, dirname } from 'path'; +import { fileURLToPath } from 'url'; +import { randomBytes } from 'crypto'; + +const __dir = dirname(fileURLToPath(import.meta.url)); + +/** Generate a Foundry-style 16-char alphanumeric ID */ +function makeId() { + const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + let id = ''; + const bytes = randomBytes(16); + for (const b of bytes) id += chars[b % chars.length]; + return id; +} + +/** + * Write an array of item objects into a Foundry LevelDB pack directory. + * @param {string} packDir - absolute path to the pack folder (will be wiped & recreated) + * @param {object[]} items - array of Foundry item documents (name, type, img, system) + */ +async function buildPack(packDir, items) { + // Wipe existing pack so we get a clean rebuild + if (existsSync(packDir)) { + rmSync(packDir, { recursive: true, force: true }); + } + + const db = new ClassicLevel(packDir, { keyEncoding: 'utf8', valueEncoding: 'utf8' }); + + for (const item of items) { + const id = makeId(); + const doc = { + _id: id, + name: item.name, + type: item.type, + img: item.img ?? 'icons/svg/item-bag.svg', + system: item.system ?? {}, + effects: [], + flags: {}, + folder: null, + sort: 0, + ownership: { default: 0 }, + _stats: { + systemId: 'slazero2', + systemVersion: '1.3.0-dev', + coreVersion: '14.359', + createdTime: Date.now(), + modifiedTime: Date.now(), + lastModifiedBy: 'builder' + } + }; + await db.put(`!items!${id}`, JSON.stringify(doc)); + } + + // Force compaction: flushes WAL (.log) into proper SST (.ldb) files + // Foundry requires .ldb format — without this the packs appear empty + await db.compactRange('\x00', '\xff'); + await db.close(); + console.log(` ✓ ${items.length} items → ${packDir.replace(__dir, '.')}`); +} + +// ── Pack definitions ───────────────────────────────────────────────────────── +// Each entry: { id, label, source JSON file } +// To add a new pack: add an entry here and create the matching JSON source file. + +const PACKS = [ + { + id: 'sla-armors', + label: 'SLA Armors', + source: 'packs/armors.json', + }, + { + id: 'sla-drugs', + label: 'SLA Drugs & Consumables', + source: 'packs/drugs.json', + }, + { + id: 'sla-ebb-formulae', + label: 'SLA Ebb Formulae', + source: 'packs/ebb-formulae.json', + }, + { + id: 'sla-specialties', + label: 'SLA Specialties', + source: 'packs/specialties.json', + }, + { + id: 'sla-weapons', + label: 'SLA Weapons', + source: 'packs/weapons.json', + }, + { + id: 'sla-equipment', + label: 'SLA Equipment & Gear', + source: 'packs/equipment.json', + }, + { + id: 'sla-ammo', + label: 'SLA Ammunition', + source: 'packs/ammo.json', + }, +]; + +// ── Run ────────────────────────────────────────────────────────────────────── +console.log('Building SLA SLA Zero Two compendium packs…\n'); + +for (const pack of PACKS) { + const sourcePath = join(__dir, pack.source); + const destDir = join(__dir, 'packs', pack.id); + const items = JSON.parse(readFileSync(sourcePath, 'utf8')); + console.log(`Building: ${pack.label} (${items.length} items)`); + await buildPack(destDir, items); +} + +console.log('\nDone. Re-run this script any time you update a source JSON file.'); diff --git a/fonts/SLABorg-Book.woff2 b/fonts/SLABorg-Book.woff2 new file mode 100644 index 0000000..4eb1f50 Binary files /dev/null and b/fonts/SLABorg-Book.woff2 differ diff --git a/fonts/SLABorg-Tech.woff2 b/fonts/SLABorg-Tech.woff2 new file mode 100644 index 0000000..107b430 Binary files /dev/null and b/fonts/SLABorg-Tech.woff2 differ diff --git a/fonts/SLABorg-Title.woff2 b/fonts/SLABorg-Title.woff2 new file mode 100644 index 0000000..01046db Binary files /dev/null and b/fonts/SLABorg-Title.woff2 differ diff --git a/fonts/SLABorg-UI.woff2 b/fonts/SLABorg-UI.woff2 new file mode 100644 index 0000000..0dc6200 Binary files /dev/null and b/fonts/SLABorg-UI.woff2 differ diff --git a/fonts/blade-runner-symbols.ttf b/fonts/blade-runner-symbols.ttf new file mode 100644 index 0000000..9b767ff Binary files /dev/null and b/fonts/blade-runner-symbols.ttf differ diff --git a/lang/de.json b/lang/de.json new file mode 100644 index 0000000..0a52c7a --- /dev/null +++ b/lang/de.json @@ -0,0 +1 @@ +{"FLBR.Attributes":"Attribute","FLBR.ATTRIBUTE.STR":"Staerke","FLBR.ATTRIBUTE.AGI":"Geschicklichkeit","FLBR.ATTRIBUTE.INT":"Intelligenz","FLBR.ATTRIBUTE.EMP":"Empathie","FLBR.ATTRIBUTE.MVR":"Wendigkeit","FLBR.Skills":"Fertigkeiten","FLBR.SKILL.CloseCombat":"Nahkampf","FLBR.SKILL.Force":"Kraft","FLBR.SKILL.Stamina":"Ausdauer","FLBR.SKILL.Firearms":"Schusswaffen","FLBR.SKILL.Mobility":"Beweglichkeit","FLBR.SKILL.Stealth":"Heimlichkeit","FLBR.SKILL.MedicalAid":"Medizinische Hilfe","FLBR.SKILL.Observation":"Beobachtung","FLBR.SKILL.Tech":"Technologie","FLBR.SKILL.Connections":"Kontakte","FLBR.SKILL.Insight":"Menschenkenntnis","FLBR.SKILL.Manipulation":"Manipulation","FLBR.SKILL.Driving":"Fahren","FLBR.NATURE.Human":"Mensch","FLBR.NATURE.Replicant":"Replikant","FLBR.ARCHETYPE.ANALYST":"Analyst","FLBR.ARCHETYPE.CITY_SPEAKER":"City Speaker","FLBR.ARCHETYPE.DOXIE":"Doxie","FLBR.ARCHETYPE.ENFORCER":"Enforcer","FLBR.ARCHETYPE.FIXER":"Fixer","FLBR.ARCHETYPE.INSPECTOR":"Inspector","FLBR.ARCHETYPE.SKIMMER":"Skimmer","FLBR.COMBAT_ACTION.Sprint":"Sprint","FLBR.COMBAT_ACTION.Crawl":"Kriechen","FLBR.COMBAT_ACTION.UnarmedAttack":"Unbewaffneter Angriff","FLBR.COMBAT_ACTION.MeleeAttack":"Nahkampf-Angriff","FLBR.COMBAT_ACTION.Grapple":"Klammerangriff","FLBR.COMBAT_ACTION.BreakFree":"Losreissen","FLBR.COMBAT_ACTION.ShootFirearm":"Waffe abfeuern","FLBR.COMBAT_ACTION.CarefulAim":"Zielen","FLBR.COMBAT_ACTION.ThrowWeapon":"Waffe werfen","FLBR.COMBAT_ACTION.FirstAid":"Erste Hilfe","FLBR.COMBAT_ACTION.Manipulate":"Manipulieren","FLBR.COMBAT_ACTION.UseItem":"Objekt benutzen","FLBR.COMBAT_ACTION_HINT.Sprint":"Du kannst in einer einzigen Runde zusätzliche Bewegungs-Aktionen durchführen, indem du die Sprint-Aktion verwendest. Dies zählt dann als deine Aktion für diese Runde, was bedeutet, dass du keine andere Aktion ausführen kannst. Würfle BEWEGLICHKEIT - für jedes gewürfelte S kannst du zusätzlich zu deiner freien Bewegung eine weitere Bewegung machen. Wenn du den Wurf nicht schaffst, fällst du (vor der zweiten Bewegung) und kannst erst in deiner nächsten Runde wieder aufstehen.","FLBR.COMBAT_ACTION_HINT.Crawl":"Wenn du dich bewegen musst, während du liegst, musst du kriechen. Dies funktioniert wie eine Bewegung, zählt aber als deine Aktion für diese Runde. Am Ende deiner Kriechbewegung kannst du aufstehen (freie Aktion).","FLBR.COMBAT_ACTION_HINT.UnarmedAttack":"Stärke + Nahkampf","FLBR.COMBAT_ACTION_HINT.MeleeAttack":"Stärke + Nahkampf","FLBR.COMBAT_ACTION_HINT.Grapple":"Wenn du deinen Gegner als Spezialangriff im Nahkampf packst, fallen sowohl du als auch dein Gegner zu Boden. Der Gegner lässt alle Waffen fallen, die er in der Hand hatte, und kann sich nicht bewegen. Als einzige erlaubte Aktion kann er versuchen sich zu befreien – Dies gelingt, wenn dem Gegner ein vergleichender Nahkampf-Wurf gegen dich gewinnt. Während du deinen Gegner im Griff hast, kannst du als einzige erlaubte Aktion (bis du deinen Gegner loslässt) einen Klammerangriff durchführen. Dieser funktioniert wie ein normaler unbewaffneter Angriff, aber dein Ziel gilt als gefesselt und kann sich daher nicht verteidigen.","FLBR.COMBAT_ACTION_HINT.BreakFree":"Stärke + Nahkampf","FLBR.COMBAT_ACTION_HINT.ShootFirearm":"Geschicklichkeit + Schusswaffen","FLBR.COMBAT_ACTION_HINT.CarefulAim":"Wenn du dir die Zeit nimmst, sorgfältig zu zielen, bevor du den Abzug drückst, erhältst du einen Vorteil für deinen Angriffswurf. Zielen ist jedoch eine eigenständige Aktion. Du kannst nicht in der gleichen Runde zielen und schießen. Wenn du nach dem Zielen irgendetwas anderes tust, als deine Waffe abzufeuern, oder wenn du von einem Angriff getroffen wirst, verlierst du den Effekt des Zielens und du musst eine weitere Aktion ausgeben, um erneut sorgfältig zu zielen.","FLBR.COMBAT_ACTION_HINT.ThrowWeapon":"Geschicklichkeit + Schusswaffen","FLBR.COMBAT_ACTION_HINT.FirstAid":"Intelligenz + Medizinische Hilfe","FLBR.COMBAT_ACTION_HINT.Manipulate":"Empathie + Manipulation","FLBR.COMBAT_ACTION_HINT.UseItem":"Objekt benutzen","FLBR.BUTTON.Delete":"Objekt entfernen","FLBR.BUTTON.Edit":"Objekt editieren","FLBR.BUTTON.Mount":"Zur Waffe greifen","FLBR.BUTTON.Roll":"Objekt würfeln","FLBR.BUTTON.ToChat":"Objekt in Chat anzeigen","FLBR.BUTTON.Unmount":"Waffe ins Holster","FLBR.CHAT_ACTION.ApplyDamage":"Schaden auf ausgewählte Figur anwenden","FLBR.DIALOG.AssignSuccesses":"Erfolg(e) zuweisen","FLBR.DIALOG.AssignSuccessesHint":"Wähle die Anzahl der Erfolge, die du gegen {name} anwenden möchtest.","FLBR.DIALOG.ChooseAction":"Wähle Aktion","FLBR.DIALOG.ChooseAttack":"Wähle Angriff","FLBR.HEADER.Actions":"Aktionen","FLBR.HEADER.Appearance":"Aussehen","FLBR.HEADER.Armor":"Panzerung","FLBR.HEADER.Attacks":"Angriffs-Arten","FLBR.HEADER.AttributesAndSkills":"Attribute & Fertigkeiten","FLBR.HEADER.Augments":"Augmentierungen","FLBR.HEADER.ChinyenPoints":"Chinyen-Punkte","FLBR.HEADER.DieSize":"Würfel","FLBR.HEADER.FavoredGear":"Bevorzugte Ausrüstung","FLBR.HEADER.Health":"Gesundheit","FLBR.HEADER.Home":"Wohnung","FLBR.HEADER.Hull":"Hülle","FLBR.HEADER.HumanityPoints":"Menschlichkeits-Punkte","FLBR.HEADER.Injuries":"Verletzungen","FLBR.HEADER.Inventory":"Ausrüstung","FLBR.HEADER.Notes":"Notizen","FLBR.HEADER.Modifiers":"Modifikatoren","FLBR.HEADER.PromotionPoints":"Beförderungs-Punkte","FLBR.HEADER.Resolve":"Entschlossenheit","FLBR.HEADER.SignatureItem":"Persönlicher Gegenstand","FLBR.HEADER.Specialties":"Spezialisierungen","FLBR.HEADER.KeyMemory":"Schlüssel-Erinnerung","FLBR.HEADER.KeyRelationship":"Schlüssel-Beziehung","FLBR.HEADER.Rating":"Stufe","FLBR.HEADER.YearsOnTheForce":"Dienstjahre","FLBR.HEADER.Weapons":"Waffen","FLBR.SHEET_CONFIG.CharacterSubtype":"Unterklasse","FLBR.SHEET_CONFIG.CharacterSubtypeHint":"Wähle aus, ob es sich um einen SC oder einen NSC handelt. Letzterer bietet einen verkleinerten Charakterbogen mit zusätzlichen Funktionen für die SL, um NSCs einfacher zu verwalten.","FLBR.SHEET_CONFIG.HealthMaxModifier":"Gesundheit (Bonus)","FLBR.SHEET_CONFIG.HealthMaxModifierHint":"Anzahl der zusätzlichen maximalen Lebenspunkte (nur SL).","FLBR.SHEET_CONFIG.HealthPermanentLoss":"Gesundheit (Malus)","FLBR.SHEET_CONFIG.HealthPermanentLossHint":"Anzahl der reduzierten maximalen Lebenspunkte (nur SL).","FLBR.SHEET_CONFIG.ResolveMaxModifier":"Entschlossenheit (Bonus)","FLBR.SHEET_CONFIG.ResolveMaxModifierHint":"Anzahl der zusätzlichen maximalen Entschlossenheitspunkte (nur SL).","FLBR.SHEET_CONFIG.ResolvePermanentLoss":"Entschlossenheit (Malus)","FLBR.SHEET_CONFIG.ResolvePermanentLossHint":"Anzahl der reduzierten maximalen Entschlossenheitspunkte (nur SL).","FLBR.SHEET_CONFIG.Maneuverability":"Fahrzeug Manövrierbarkeit","FLBR.SHEET_CONFIG.ManeuverabilityHint":"Legt die Manövrierbarkeit des von dieser Figur gefahrenen Fahrzeugs fest, falls zutreffend.","FLBR.SHEET_CONFIG.UnlimitedPush":"Erlaube unbegrenztes Strapazieren des Wurfs","FLBR.SHEET_CONFIG.UnlimitedPushHint":"Wenn die SL eine unbegrenzte Anzahl von strapazierten Würfen erlaubt. Aktiviere diese Einstellung, um bei fast jedem Wurf einen Strapazieren-Knopf anzuzeigen.","FLBR.SHEET_HEADER.ArmorRoll":"Würfle auf Panzerung","FLBR.SHEET_HEADER.BlastPowerRoll":"Würfle auf Sprengkraft","FLBR.SHEET_HEADER.GenericRoll":"Standard Wurf","FLBR.SHEET_HEADER.ItemPost":"Zeige Objekt an","FLBR.SHEET_HEADER.ItemRoll":"Würfle auf Objekt","FLBR.TAB.Actions":"Aktionen","FLBR.TAB.Bio":"Hintergrund","FLBR.TAB.Combat":"Kampf","FLBR.TAB.Info":"Information","FLBR.TAB.Inventory":"Ausrüstung","FLBR.TAB.Features":"Besonderheiten","FLBR.TAB.Mods":"Besonderheiten","FLBR.TAB.Stats":"Werte","FLBR.D":"D","FLBR.NO_VALUE":"–","FLBR.ACTIVE_EFFECT.Add":"Addieren einen aktiven Effekt","FLBR.ACTIVE_EFFECT.Delete":"Entferne Effect","FLBR.ACTIVE_EFFECT.Edit":"Editiere Effekt","FLBR.ACTIVE_EFFECT.Empty":"Kein Effect.","FLBR.ACTIVE_EFFECT.New":"Neuer Effekt","FLBR.ACTIVE_EFFECT.Toggle":"Schalte Effekt ein/aus","FLBR.ACTIVE_EFFECT.InActorDisclaimer":"Hinweis: Aktive Effekte können nicht in eingebundenen Objekten editiert werden (Limitierung durch FoundryVTT).","FLBR.Advantage":"Vorteil","FLBR.Capacities":"Gesundheit und Entschlossenheit","FLBR.Blast":"Explosion","FLBR.BlastPower":"Sprengkraft","FLBR.Broken":"GEBROCHEN","FLBR.BrokenByDamage":"GEBROCHEN DURCH SCHADEN","FLBR.BrokenByStress":"GEBROCHEN DURCH STRESS","FLBR.Cancel":"Abbrechen","FLBR.CHAT.DamageDealt":"{name} erleidet {amount} Schaden.","FLBR.CHAT.NoDamageDealt":"{name} erleidet keinen Schaden.","FLBR.CHAT.InitialDamageDealt":"Der Angriff erzielte initial {damage} Schaden.","FLBR.CHAT.ArmorDeflectedDamage":"Panzerung wehrte {damage} Schaden ab.","FLBR.CHAT.ArmorDeflectedAllDamage":"Panzerung wehrte den komplette Schaden ab.","FLBR.CHAT.ArmorNoEffect":"Panzerung hat keinen Effekt.","FLBR.CHAT.CritInflicted":"{name} erleidet die kritische Verletzung {crit}.","FLBR.COMBAT.Combat":"Kampf","FLBR.COMBAT.CannotBeDamaged":"Kann keinen Schaden erleiden","FLBR.COMBAT.DrawSize":"Bestimme Größe","FLBR.COMBAT.SkippedDefender":"Übersprungener Verteidiger {name}","FLBR.CreateNewItem":"Erstelle ein neues {type} Objekt","FLBR.Crit":"Krit","FLBR.CRIT.CritButtonTooltip":"Erstelle ein KRIT für den SC des Spielers oder ein ausgewähltes Token","FLBR.CRIT.CriticalInjury":"Kritische Verletzung","FLBR.CRIT.ChooseCrit":"Wähle eine Kritische Verletzung","FLBR.CRIT.ChooseVehicleDamage":"Wähle eine Kritische Fahrzeug Beschädigung","FLBR.CRIT.ChooseActor":"Wähle einen Actor um eine Kritische Verletzung zu verursachen.","FLBR.CRIT.DrawCrit":"Ermittle Kritische Verletzung","FLBR.CRIT.DrawVehicleDamage":"Ermittle Kritischen Fahrzeug-Schaden","FLBR.CustomFormula":"Eigene Formel","FLBR.Disadvantage":"Nachteil","FLBR.Formula":"Formel","FLBR.InjuryCategory":"Kategorie","FLBR.InjuryDeathSave":"Rettungswurf","FLBR.InjuryHealing":"Heilung","FLBR.InjuryLethal":"Toedlich","FLBR.InjuryPermanent":"Permanent","FLBR.InventoryNoItem":"Leer.","FLBR.ItemAddAction":"Aktion hinzufügen","FLBR.ItemAddAttack":"Angriff hinzufügen","FLBR.ItemAddModifier":"Wurf-Modifizierung hinzufügen","FLBR.ItemActionAttribute":"Attribut","FLBR.ItemActionName":"Aktions-Name","FLBR.ItemActionSkill":"Fertigkeit","FLBR.ItemActionType":"Aktionstyp","FLBR.ItemActionTypeRollSkill":"Fertigkeits-Wurf","FLBR.ItemActionTypeRunMacro":"Starte Macro","FLBR.ItemAttackName":"Angriffs-Name","FLBR.ItemArmor":"Panzerung","FLBR.ITEM_AVAILABILITY.Incidental":"Zufaelllig","FLBR.ITEM_AVAILABILITY.Luxury":"Luxus","FLBR.ITEM_AVAILABILITY.Premium":"Premium","FLBR.ITEM_AVAILABILITY.Rare":"Rar","FLBR.ITEM_AVAILABILITY.Standard":"Standard","FLBR.ItemAvailability":"Verfuegbarkeit","FLBR.ItemConsumable":"Verbrauchsgut","FLBR.ItemConsumeNotif":"{qty} Einheiten von {name} wurden verbraucht","FLBR.ItemCost":"Kosten","FLBR.ItemDeleteAction":"Aktion entfernen","FLBR.ItemDeleteAttack":"Angriff entfernen","FLBR.ItemDeleteModifier":"Modifikation entfernen","FLBR.ItemNewAction":"Neue Aktion","FLBR.ItemNewAttack":"Neuer Angriff","FLBR.ItemNoAttack":"Kein Angriffs-Modus definiert.","FLBR.ItemNoModifier":"Kein Modifikator.","FLBR.ItemModifier":"Modifikator","FLBR.ItemMounted":"Angelegt","FLBR.ItemQuantity":"Anzahl","FLBR.ItemSpecial":"Spezialisierung","FLBR.MACRO.GetActorTitle":"Wähle eine Figur","FLBR.MACRO.GetActorHint":"Du hast mehrere Figuren ausgewählt. Bitte wähle eine davon aus.","FLBR.MACRO.MultipleItems":"Deine zugewiesene Figur {actor} hat mehrere Objekte mit dem Namen {item}. Das erste gefundene Objekt wird genutzt.","FLBR.MACRO.NoActor":"Deine zugewiesene Figur {actor} wurde nicht gefunden.","FLBR.MACRO.NoItem":"Deine zugewiesene Figur {actor} besitzt kein Objekt mit dem Namen {item}.","FLBR.MACRO.RollAction":"Aktion {action}","FLBR.MACRO.RollStat":"Würfle {stat}","FLBR.Nature":"Abstammung","FLBR.NatureHint":"Abstammung des Characters (SL-Modifikation)","FLBR.NewItem":"Neu - {type}","FLBR.OK":"OK","FLBR.Range":"Reichweite","FLBR.RANGED_COMBAT.OutOfRange":"Außer Reichweite","FLBR.RANGED_COMBAT.SmallTarget":"Kleines Ziel","FLBR.RANGED_COMBAT.LargeTarget":"Großes Ziel","FLBR.RANGED_COMBAT.TargetBehindCover":"Ziel in Deckung","FLBR.RANGED_COMBAT.CarefulAim":"Zielen","FLBR.RANGED_COMBAT.UnseenTarget":"Nicht sichtbares Ziel","FLBR.ROLLER.AddDie":"Würfel hinzufügen","FLBR.ROLLER.MaxPush":"Max. Strapazieren","FLBR.ROLLER.ModifierActive":"Aktiv","FLBR.ROLLER.ModifierName":"Name","FLBR.ROLLER.ModifierValue":"Wert","FLBR.ROLLER.TotalModifier":"Insgesamt: {value}","FLBR.ROLLER.ResolveTest":"Wurf für permanenten Verlust von Entschlossenheit","FLBR.ROLLER.ResolveTestHint":"Wenn du durch Stress gebrochen bist, musst du einen Basis-Würfel werfen, der deinem Empathiewert entspricht. Wenn du eine Eins würfelst, verlierst du permanent 1 Punkt Entschlossenheit, d.h. dein maximaler Entschlossenheit Wert wird um einen Punkt reduziert.","FLBR.ROLLER.ResolveTestFailed":"Der Empathie-Wurf von {name} ist fehlgeschlagen. Daher sinkt ENTSCHLOSSENHEIT permanent um 1.","FLBR.ROLLER.RollButton":"Wurf","FLBR.ROLLER.RollSpecialHint":"Roll: {name}?","FLBR.ROLLER.RollWithAdvantageButton":"Vorteil","FLBR.ROLLER.RollWithDisadvantageButton":"Nachteil","FLBR.ROLLER.SelectDiceToPush":"Wähle zu strapazierende Würfel","FLBR.ROLLER.UnlimitedPush":"Unbegrenzt","FLBR.SignatureItemUsedCheckboxHint":"Benutzt?","FLBR.SomeoneIsSomething":"{name} is {status}","FLBR.VEHICLE.Action.Crash":"Absturz","FLBR.VEHICLE.Action.CrashHint":"Wenn ein Spinner zerstört wurde, stürzt er auf den Boden. Alle Insassen erleiden W3 Punkte Fallschaden plus einen für jede Höhen-Zone, gemildert durch die Panzerung des Fahrzeugs und einen BEWEGLICHKEIT-Wurf (keine Aktion): Jeder Erfolg senkt den Schaden um 1.","FLBR.VEHICLE.Action.CrashDialog":"Das Fahrzeug stürzt ab und verursacht bei allen Insassen {damage} Fallschaden, abgemildert durch die Panzerung des Fahrzeugs und einen BEWEGLICHKEIT-Wurf (keine Aktion): Jeder Erfolg senkt den Schaden um 1.","FLBR.VEHICLE.Action.Explode":"Explosion","FLBR.VEHICLE.Action.ExplodeHint":"Das Fahrzeug explodiert in einem Feuerball. Alle Insassen erleiden Schaden in Höhe einer Sprengkraft B Explosion. Das Fahrzeug ist irreparabel zerstört.","FLBR.VEHICLE.Action.MassiveCrash":"Schwerer Aufprall","FLBR.VEHICLE.Action.Ramming":"Rammen","FLBR.VEHICLE.Action.RammingHint":"Die meisten Fahrzeuge können als Waffen genutzt werden - bspw. indem man das Ziel überfährt. Dies zählt als NAHKAMPF Angriff, der mit einem Wurf auf FAHREN durchgeführt wird. Der Wurf kann mit einer Beschleunigungs-Aktion kombiniert werden, in diesem Fall ist der FAHREN-Wurf ein vergleichender Wurf, was die Effektivität der Beschleunigung einschränken kann. Der Basis-Schaden der Attacke entspricht dem HÜLLEN-Wert des Fahrzeugs geteilt durch 2 (abgerundet). Ramm-Angriffe verursachen Stumpfen Schaden mit einem KRIT-Würfel der dem HÜLLEN-Wert des Fahrzeugs entspricht (aufgerundet).","FLBR.VEHICLE.Action.Repair":"Reparieren","FLBR.VEHICLE.Action.RepairHint":"Reparaturen von Fahrzeugschaden benötigen ein oder mehrere Würfe auf TECHNOLOGIE. Pro SCHICHT kann ein Wurf ablegegt werden. Nur eien Person kann auf Hüllen-Reparaturen würfeln, aber weitere können helfen. Jedes gewürfelte S reduziert den Schaden um 1. Ein Fahrzeug gilt nicht mehr als zerstört, wenn es 1 Schadenspunkt repairet wurde. Wenn der Motor oder eine Fahrzeugwaffe durch einen kritischen Treffer ausgefallen ist, so müssen diese normalerweise mit einem zusätzlichen Wurf auf TECNHNOLOGIE repariert werden. Auch dieser Wurf dauert eine SCHICHT.","FLBR.VEHICLE.Action.Speeding":"Beschleunigen","FLBR.VEHICLE.Action.SpeedingHint":"Ähnlich wie bei einem Sprint zu Fuß kannst du über die Beschleunigungs-Aktion für ein Fahrzeug zusätzliche Manöver-Aktionen in einer Runde erhalten.","FLBR.VEHICLE.Altitude":"Hoehe","FLBR.VEHICLE.AddSeat":"Erhöhe die Insassen Kapazität (nur SL)","FLBR.VEHICLE.ChoosePassenger":"Wähle Insasse","FLBR.VEHICLE.ChoosePassengerHint":"Wähle einen der Insassen.","FLBR.VEHICLE.Components":"Komponenten","FLBR.VEHICLE.CrashDamageFlavor":"Schaden den die Crew durch den Absturz ihres Fahrzeugs erleidet","FLBR.VEHICLE.CreateActor":"Erstelle Actor?","FLBR.VEHICLE.CreateActorHint":"Möchtest du den Actor {name} duplizieren, bevor du ihn zur Crew machst?","FLBR.VEHICLE.Crew":"Crew","FLBR.VEHICLE.DecreaseHull":"Reduziere Max. Hülle","FLBR.VEHICLE.DeleteSeat":"Reduziere Insassen Kapazität","FLBR.VEHICLE.EditPassenger":"Editiere Insasse","FLBR.VEHICLE.Engine":"Motor","FLBR.VEHICLE.IncreaseHull":"Erhöhe Max. Hülle","FLBR.VEHICLE.MountedWeapons":"Fahrzeug-Waffen","FLBR.VEHICLE.NotifCrewFull":"Keine freien Sitze mehr!","FLBR.VEHICLE.NotifDropCrewVehicle":"Nicht erlaubt!","FLBR.VEHICLE.NotifDropCrewLoot":"Wirf deinen Abfall besser in den Mülleimer!","FLBR.VEHICLE.NotifPassengerAlreadyPresent":"Insasse ist bereits platziert!","FLBR.VEHICLE.RemovePassenger":"Entferne Insasse","FLBR.WeaponDamage":"Schaden","FLBR.WeaponCrit":"Krit Würfel","FLBR.WeaponDamageType":"Schadenstyp","FLBR.WEAPON_DAMAGE_TYPE.None":"–","FLBR.WEAPON_DAMAGE_TYPE.Crushing":"Stumpf","FLBR.WEAPON_DAMAGE_TYPE.Piercing":"Stich","FLBR.WEAPON_DAMAGE_TYPE.Stress":"Stress","FLBR.WeaponRange":"Reichweite","FLBR.WEAPON_RANGE.Engaged":"Kontakt","FLBR.WEAPON_RANGE.Short":"Nah","FLBR.WEAPON_RANGE.Medium":"Mittel","FLBR.WEAPON_RANGE.Long":"Weit","FLBR.WEAPON_RANGE.Extreme":"Extrem","FLBR.Wrecked":"Zerstört","FLBR.YouAreBroken":"Du bist GEBROCHEN!","FLBR.ActorSheet.NotifWrongItemType":"Figur {actor} kann keine {type} Objekte tragen!","YZEC.CombatTracker.FastAction":"Bewegt","YZEC.CombatTracker.SlowAction":"Agiert","YZUR.CHAT.ROLL.Infos":"Info","YZUR.CHAT.ROLL.Damage":"Basis-Schaden","YZUR.CHAT.ROLL.Push":"Strapazieren","YZUR.CHAT.ROLL.CancelPush":"Akzeptieren","YZUR.DIETERMS.D6BladeRunnerDie":"D6","YZUR.DIETERMS.D8BladeRunnerDie":"D8","YZUR.DIETERMS.D10BladeRunnerDie":"D10","YZUR.DIETERMS.D12BladeRunnerDie":"D12","TYPES.Actor.character":"Charakter","TYPES.Actor.vehicle":"Fahrzeug","TYPES.Actor.loot":"Fund","ACTOR.SubtypePc":"SC","ACTOR.SubtypeNpc":"NSC","TYPES.Item.generic":"Standard","TYPES.Item.upgrade":"Augmentierung","TYPES.Item.armor":"Panzerung","TYPES.Item.weapon":"Waffe","TYPES.Item.explosive":"Explosiv","TYPES.Item.specialty":"Spezialisierung","TYPES.Item.injury":"Verletzung","GAME.Paused":"Ermittlung pausiert","SETTINGS.BLADE_RUNNER.AutomaticApplyDamageName":"Automatisch Schaden zuweisen","SETTINGS.BLADE_RUNNER.AutomaticApplyDamageHint":"Bestimmt, ob Schaden auf ausgewählte Ziele automatisch zugewiesen werden soll.","SETTINGS.BLADE_RUNNER.AutomaticArmorRollName":"Automatischer Panzerungs-Wurf","SETTINGS.BLADE_RUNNER.AutomaticArmorRollHint":"Bestimmt, ob automatisch auf Panzerung gewürfelt werden soll.","SETTINGS.BLADE_RUNNER.CrashTableName":"Vehicle Damage Table","SETTINGS.BLADE_RUNNER.CrashTableHint":"ID oder Name der kritischen Fahrzeug-Schaden Tabelle.","SETTINGS.BLADE_RUNNER.CrushingTableName":"Crushing Table","SETTINGS.BLADE_RUNNER.CrushingTableHint":"ID oder Name der Kritische Stumpfe Verletzungen Tabelle.","SETTINGS.BLADE_RUNNER.PiercingTableName":"Piercing Table","SETTINGS.BLADE_RUNNER.PiercingTableHint":"ID oder Name der Kritische Stich Verletzungen Tabelle.","SETTINGS.BLADE_RUNNER.EditNaturePermissionName":"Erlaube Veränderung der Abstammung","SETTINGS.BLADE_RUNNER.EditNaturePermissionHint":"Erlaube dem Spieler die Abstammung seines Charakters zu verändern.","SETTINGS.BLADE_RUNNER.OpenFirstWeaponAttackName":"Zeige ersten Waffen Angriff","SETTINGS.BLADE_RUNNER.OpenFirstWeaponAttackHint":"Bestimmt, ob bei der Anzeige eines Waffen-Objekts automatisch der erste Waffen-Angriff des Objektes ausgeklappt dargestellt werden soll.","SETTINGS.BLADE_RUNNER.UpdateActorManeuverabilityOnCrewName":"Aktualisiere Manövrierbarkeit bei Crew-Beitritt","SETTINGS.BLADE_RUNNER.UpdateActorManeuverabilityOnCrewHint":"Erlaube die Änderung der Manövrierbarkeit eines Actors, wenn er einer Fahrzeug-Crew zugewiesen wurde.","SETTINGS.BLADE_RUNNER.UpdateActorManeuverabilityOnUncrewName":"Aktualisiere Manövrierbarkeit bei Crew-Entfernung","SETTINGS.BLADE_RUNNER.UpdateActorManeuverabilityOnUncrewHint":"Erlaube die Änderung der Manövrierbarkeit eines Actors, wenn er von einer Fahrzeug-Crew entfernt wurde.","SETTINGS.BLADE_RUNNER.UseActiveEffectsName":"Benutze Aktive Effekte","SETTINGS.BLADE_RUNNER.UseActiveEffectsHint":"Aktiviere die Anzeige eines zusätzliches Feldes im Charakter-Blatt für das Einstellen von Aktiven Effekten (für fortgeschrittene Spieler).","WARNING.ApplyDamageNoNature":"Zuweisung des Strapazierungs-Schadens: Figur-Abstammung nicht gefunden!","WARNING.ApplyDamageNoCapacityKey":"Zuweisung des Strapazierungs-Schadens: Abstammung-Kapazität Verbindung nicht gefunden!","WARNING.ApplyDamageNoActorCapacity":"Zuweisung des Strapazierungs-Schadens: Figur-Kapazität nicht gefunden!","WARNING.NoDiceInput":"Keine Würfel zum werfen!","WARNING.NoItemAction":"Keine Objekt Aktionen definiert!","WARNING.ItemActionMacroNotFound":"Macro \"{macro}\" nicht gefunden!","WARNING.NoItemUnit":"Kann {name} nicht benutzen: Vorrat erschöpft!"} \ No newline at end of file diff --git a/lang/en.json b/lang/en.json new file mode 100644 index 0000000..1c6889d --- /dev/null +++ b/lang/en.json @@ -0,0 +1,3 @@ +{ + "ZERO.SystemName": "SLA Zero Two" +} \ No newline at end of file diff --git a/lang/es.json b/lang/es.json new file mode 100644 index 0000000..378d286 --- /dev/null +++ b/lang/es.json @@ -0,0 +1 @@ +{"FLBR.ATTRIBUTE.AGI":"Agilidad","FLBR.ATTRIBUTE.EMP":"Empatía","FLBR.ATTRIBUTE.MVR":"Maniobrabilidad","FLBR.Skills":"Habilidades","FLBR.ATTRIBUTE.STR":"Fortaleza","FLBR.SKILL.Force":"Fuerza","FLBR.SKILL.Stamina":"Resistencia","FLBR.SKILL.Firearms":"Disparar","FLBR.SKILL.Mobility":"Movilidad","FLBR.SKILL.Stealth":"Sigilo","FLBR.SKILL.MedicalAid":"Primeros Auxilios","FLBR.SKILL.Tech":"Tecnología","FLBR.SKILL.Connections":"Conexiones","FLBR.SKILL.Insight":"Perspicacia","FLBR.SKILL.Manipulation":"Manipulación","FLBR.SKILL.Driving":"Conducción","FLBR.NATURE.Human":"Humano","FLBR.NATURE.Replicant":"Replicante","FLBR.ARCHETYPE.ANALYST":"Analista","FLBR.ARCHETYPE.CITY_SPEAKER":"Informante","FLBR.COMBAT_ACTION.Sprint":"Correr","FLBR.COMBAT_ACTION.Crawl":"Arrastrarrse","FLBR.COMBAT_ACTION.UnarmedAttack":"Ataque desarmado","FLBR.COMBAT_ACTION.MeleeAttack":"Ataque Cuerpo a Cuerpo","FLBR.COMBAT_ACTION.BreakFree":"Liberarse","FLBR.COMBAT_ACTION.Grapple":"Apresar","FLBR.COMBAT_ACTION.ShootFirearm":"Disparar arma","FLBR.COMBAT_ACTION.CarefulAim":"Apuntar","FLBR.COMBAT_ACTION.FirstAid":"Primeros Auxilios","FLBR.COMBAT_ACTION.Manipulate":"Manipular","FLBR.COMBAT_ACTION.UseItem":"Usar objeto","FLBR.ARCHETYPE.DOXIE":"Maleante","FLBR.ARCHETYPE.SKIMMER":"Corrupto","FLBR.ARCHETYPE.FIXER":"Confiable","FLBR.ARCHETYPE.ENFORCER":"Tipo duro","FLBR.SKILL.CloseCombat":"Luchar","FLBR.COMBAT_ACTION_HINT.Crawl":"Si necesitas moverte mientras estás tumbado, debes arrastrarte. Esto funciona igual que moverse, pero cuenta como Acción de la Ronda. Puedes levantarte al final del movimiento como acción gratuita.","FLBR.COMBAT_ACTION_HINT.UnarmedAttack":"Fortaleza + Luchar","FLBR.COMBAT_ACTION_HINT.MeleeAttack":"Fortaleza + Luchar","FLBR.COMBAT_ACTION_HINT.ShootFirearm":"Agilidad + Disparar","FLBR.COMBAT_ACTION_HINT.ThrowWeapon":"Agilidad + Disparar","FLBR.COMBAT_ACTION_HINT.FirstAid":"Inteligencia + Primeros Auxilios","FLBR.COMBAT_ACTION_HINT.Manipulate":"Empatía + Manipulación","FLBR.COMBAT_ACTION_HINT.UseItem":"Usar Objeto","FLBR.BUTTON.Delete":"Eliminar objeto","FLBR.BUTTON.Edit":"Editar objeto","FLBR.BUTTON.Roll":"Hacer tirada","FLBR.BUTTON.ToChat":"Enviar objeto al chat","FLBR.DIALOG.AssignSuccesses":"Asignar éxito(s)","FLBR.DIALOG.ChooseAction":"Elige Acción","FLBR.DIALOG.ChooseAttack":"Elige Ataque","FLBR.HEADER.Actions":"Acciones","FLBR.HEADER.Appearance":"Apariencia","FLBR.HEADER.Armor":"Armadura","FLBR.HEADER.Attacks":"Modos de Ataque","FLBR.HEADER.AttributesAndSkills":"Atributos y Habilidades","FLBR.HEADER.Augments":"Aumentos","FLBR.HEADER.ChinyenPoints":"Chinyens","FLBR.HEADER.FavoredGear":"Equipo Predilecto","FLBR.HEADER.Home":"Hogar","FLBR.HEADER.Injuries":"Heridas","FLBR.HEADER.Notes":"Notas","FLBR.HEADER.Modifiers":"Modificadores","FLBR.HEADER.Resolve":"Resiliencia","FLBR.HEADER.SignatureItem":"Objeto característico","FLBR.HEADER.Specialties":"Especialidades","FLBR.HEADER.KeyMemory":"Recuerdo Clave","FLBR.HEADER.KeyRelationship":"Relación Clave","FLBR.HEADER.Rating":"Índice","FLBR.HEADER.YearsOnTheForce":"Años de Servicio","FLBR.HEADER.Weapons":"Armas","FLBR.SHEET_CONFIG.HealthMaxModifier":"Modificador máx. Salud","FLBR.SHEET_CONFIG.HealthMaxModifierHint":"Modifica la capacidad de salud máxima (Solo Máster).","FLBR.SHEET_CONFIG.HealthPermanentLoss":"Pérdida de Salud permanente","FLBR.SHEET_CONFIG.HealthPermanentLossHint":"Indica cuanta Salud se ha perdido permanentemente (Solo Máster).","FLBR.SHEET_CONFIG.ResolveMaxModifier":"Modificador máx. Resiliencia","FLBR.SHEET_CONFIG.ResolveMaxModifierHint":"Modifica la capacidad máxima de resiliencia (Solo Máster).","FLBR.SHEET_CONFIG.ResolvePermanentLoss":"Pérdida de Resiliencia permanente","FLBR.SHEET_CONFIG.UnlimitedPush":"Permite Forzar Tiradas Ilimitadamente","FLBR.SHEET_HEADER.ArmorRoll":"Hacer tirada de Armadura","FLBR.SHEET_HEADER.BlastPowerRoll":"Hacer tirada de Poder de Detonación","FLBR.SHEET_HEADER.GenericRoll":"Tirada Genérica","FLBR.TAB.Bio":"Biografía","FLBR.TAB.Info":"Info","FLBR.TAB.Inventory":"Inventario","FLBR.TAB.Features":"Características","FLBR.TAB.Mods":"Mods","FLBR.TAB.Stats":"Estadísticas","FLBR.D":"D","FLBR.Capacities":"Salud y Resiliencia","FLBR.Blast":"Detonación","FLBR.BlastPower":"Poder de Detonación","FLBR.Broken":"INCAPACITADO","FLBR.BrokenByStress":"INCAPACITADO POR ESTRÉS","FLBR.CHAT.NoDamageDealt":"{name} no ha recibido daño.","FLBR.CHAT.InitialDamageDealt":"El ataque tenía un daño inicial de {damage}.","FLBR.CHAT.ArmorDeflectedAllDamage":"La armadura bloqueó todo el dañó.","FLBR.CHAT.ArmorNoEffect":"La armadura no tuvo efecto.","FLBR.Disadvantage":"Desventaja","FLBR.InjuryDeathSave":"Tirada de Muerte","FLBR.InjuryHealing":"Sanación","FLBR.InventoryNoItem":"Vacío.","FLBR.ItemAddAttack":"Añadir Ataque","FLBR.ItemAddModifier":"Añadir Modificador de tirada","FLBR.ItemActionAttribute":"Atributo","FLBR.ItemActionName":"Nombre de la Acción","FLBR.ItemActionSkill":"Habilidad","FLBR.ItemActionType":"Tipo de Acción","FLBR.ItemActionTypeRollSkill":"Tirada de Habilidad","FLBR.ItemAttackName":"Nombre del Ataque","FLBR.ItemArmor":"Armadura","FLBR.ITEM_AVAILABILITY.Luxury":"Lujoso","FLBR.ITEM_AVAILABILITY.Premium":"Premium","FLBR.ITEM_AVAILABILITY.Rare":"Raro","FLBR.ITEM_AVAILABILITY.Standard":"Estándar","FLBR.ItemAvailability":"Disponibilidad","FLBR.ItemConsumable":"Consumible","FLBR.ItemCost":"Coste","FLBR.ItemDeleteAction":"Eliminar acción","FLBR.ItemDeleteAttack":"Eliminar ataque","FLBR.ItemDeleteModifier":"Eliminar modificador","FLBR.ItemNewAction":"Nueva acción","FLBR.ItemNewAttack":"Nuevo ataque","FLBR.ItemNoAttack":"No se ha definido un modo de ataque.","FLBR.ItemModifier":"Modificador","FLBR.MACRO.NoActor":"No se ha encontrado a tu personaje {actor}.","FLBR.MACRO.RollAction":"Acción {action}","FLBR.NewItem":"Nuevo {type}","FLBR.OK":"OK","FLBR.Range":"Rango","FLBR.RANGED_COMBAT.OutOfRange":"Fuera de rango","FLBR.RANGED_COMBAT.LargeTarget":"Objetivo grande","FLBR.RANGED_COMBAT.TargetBehindCover":"Objetivo tras cobertura","FLBR.ROLLER.AddDie":"Añadir un dado","FLBR.ROLLER.ModifierName":"Nombre","FLBR.ROLLER.ModifierValue":"Valor","FLBR.ROLLER.TotalModifier":"Total: {value}","FLBR.ROLLER.RollButton":"Tirada","FLBR.ROLLER.RollWithAdvantageButton":"Ventaja","FLBR.ROLLER.RollWithDisadvantageButton":"Desventaja","FLBR.ROLLER.SelectDiceToPush":"Selecciona un dado para forzar","FLBR.ROLLER.UnlimitedPush":"Ilimitado","FLBR.WeaponDamage":"Daño","FLBR.WeaponCrit":"Dado Crítico","FLBR.WeaponDamageType":"Tipo de Daño","FLBR.WEAPON_DAMAGE_TYPE.Crushing":"Contundente","FLBR.WEAPON_DAMAGE_TYPE.Piercing":"Perforante","FLBR.WEAPON_DAMAGE_TYPE.Stress":"Estrés","FLBR.WeaponRange":"Rango","FLBR.WEAPON_RANGE.Short":"Corto","FLBR.WEAPON_RANGE.Medium":"Medio","FLBR.WEAPON_RANGE.Extreme":"Extremo","YZUR.CHAT.ROLL.Infos":"Infos","YZUR.CHAT.ROLL.Damage":"Daño Base","YZUR.CHAT.ROLL.Push":"Forzar","YZUR.DIETERMS.D8BladeRunnerDie":"D8","YZUR.DIETERMS.D10BladeRunnerDie":"D10","YZUR.DIETERMS.D12BladeRunnerDie":"D12","TYPES.Actor.character":"Personaje","ACTOR.SubtypeNpc":"Personaje No Jugador","TYPES.Item.generic":"Genérico","TYPES.Item.upgrade":"Aumento","TYPES.Item.weapon":"Arma","TYPES.Item.specialty":"Especialidad","TYPES.Item.injury":"Herida","GAME.Paused":"Investigación en Pausa","SETTINGS.BLADE_RUNNER.AutomaticArmorRollName":"Tirada de Armadura Automática","WARNING.ApplyDamageNoActorCapacity":"Infligir Daño de Forzaje: ¡No se ha encontrado Capacidad del Personaje!","WARNING.NoDiceInput":"¡No se han seleccionado dados!","WARNING.ItemActionMacroNotFound":"¡Macro {macro} no encontrada!","FLBR.Attributes":"Atributos","FLBR.ATTRIBUTE.INT":"Inteligencia","FLBR.SKILL.Observation":"Observación","FLBR.ARCHETYPE.INSPECTOR":"Inspector","FLBR.COMBAT_ACTION.ThrowWeapon":"Lanzar arma","FLBR.COMBAT_ACTION_HINT.Sprint":"Puedes realizar desplazamientos adicionales en la Ronda utilizando la acción de correr. Esto contará como tu Maniobra de la Ronda, lo cual implica que no podrás realizar ninguna otra Acción. Haz una tirada de MOVILIDAD – por cada S obtenido,puedes hacer un movimiento más, además de tu movimiento gratuito. Si fallas la tirada, te caerás (antes del segundo movimiento) y no podrás levantarte hasta tu próximo turno.","FLBR.COMBAT_ACTION_HINT.Grapple":"Si apresas a tu oponente como ataque especial al luchar, ambos caéis al suelo. El oponente suelta cualquier arma que llevase en las manos y no puede moverse. La única acción que puede realizar es intentar liberarse – lo cual es una acción que tendrá éxito si el oponente gana una tirada enfrentada de LUCHAR contra ti. Mientras estás apresando, la única acción que puedes realizar (hasta que liberes a tu oponente) es un ataque de presa. Esto funciona como un ataque cuerpo a cuerpo normal, pero tu oponente contará como retenido y no podrá defenderse.","FLBR.COMBAT_ACTION_HINT.BreakFree":"Fortaleza + Luchar","FLBR.COMBAT_ACTION_HINT.CarefulAim":"Si te tomas tu tiempo para apuntar cuidadosamente antes de apretar el gatillo, obtendrás ventaja en tu tirada de ataque. Apuntar, sin embargo, es una Acción por si misma. No puedes apuntar y disparar en la misma Ronda. Si haces algo aparte de disparar tu arma tras haber apuntad, o si eres golpeado por un ataque, perderás el efecto de apuntar y tendrás que gastar otra acción para volver a apuntar.","FLBR.CHAT_ACTION.ApplyDamage":"Infligir daño a las miniaturas seleccionadas","FLBR.DIALOG.AssignSuccessesHint":"Elige un número de éxitos para gastar contra {name}.","FLBR.HEADER.DieSize":"Dado Base","FLBR.HEADER.Health":"Salud","FLBR.HEADER.HumanityPoints":"Humanidad","FLBR.HEADER.Inventory":"Inventario","FLBR.HEADER.PromotionPoints":"Puntos de Promoción","FLBR.SHEET_CONFIG.CharacterSubtype":"Subtipo","FLBR.SHEET_CONFIG.CharacterSubtypeHint":"Selecciona si es un PJ o un PNJ. El último tiene una ficha de personaje más sencilla y funcionalidad añadida para el Máster.","FLBR.SHEET_CONFIG.ResolvePermanentLossHint":"Indica cuanta Resiliencia se ha perdido permanentemente (Solo Máster).","FLBR.SHEET_CONFIG.Maneuverability":"Maniobrabilidad del Vehículo","FLBR.SHEET_CONFIG.ManeuverabilityHint":"Fija la maniobrabilidad del vehículo conducido por este personaje, si se da el caso.","FLBR.SHEET_CONFIG.UnlimitedPushHint":"Si tu Máster lo permite, puedes realizar un número ilimitado de tiradas forzadas. Marca esta opción para mostrar un botón de Forzar tirada en prácticamente cualquier tirada.","FLBR.SHEET_HEADER.ItemPost":"Publicar Objeto","FLBR.SHEET_HEADER.ItemRoll":"Hacer tirada de Objeto","FLBR.TAB.Actions":"Acciones","FLBR.TAB.Combat":"Combate","FLBR.Advantage":"Ventaja","FLBR.BrokenByDamage":"INCAPACITADO POR DAÑO","FLBR.Cancel":"Cancelar","FLBR.CHAT.DamageDealt":"{name} sufre {amount} de daño.","FLBR.CHAT.ArmorDeflectedDamage":"La armadura bloqueó {damage} de daño.","FLBR.CreateNewItem":"Crea un nuevo objeto de tipo {type}","FLBR.Crit":"Crit","FLBR.InjuryCategory":"Categoría","FLBR.InjuryLethal":"Letal","FLBR.InjuryPermanent":"Permanente","FLBR.ItemAddAction":"Añadir Acción","FLBR.ItemActionTypeRunMacro":"Ejecutar macro","FLBR.ITEM_AVAILABILITY.Incidental":"Incidente","FLBR.ItemConsumeNotif":"Ha consumido {qty} unidades de {name}","FLBR.ItemNoModifier":"Sin modificador.","FLBR.ItemQuantity":"Cantidad","FLBR.ItemSpecial":"Especial","FLBR.MACRO.GetActorTitle":"Selecciona una Armadura","FLBR.MACRO.GetActorHint":"Has seleccionado múltiples miniaturas. Por favor, selecciona solo una.","FLBR.MACRO.MultipleItems":"Tu personaje {actor} tiene más de un objeto llamado {item}. El primero que se encuentre será el seleccionado.","FLBR.MACRO.NoItem":"Tu personaje {actor} no tiene ningún objeto llamado {item}.","FLBR.MACRO.RollStat":"Hacer tirada de {stat}","FLBR.Nature":"Condición","FLBR.NatureHint":"Condición del Personaje (Sólo Máster)","FLBR.RANGED_COMBAT.SmallTarget":"Objetivo pequeño","FLBR.RANGED_COMBAT.CarefulAim":"Apuntando","FLBR.RANGED_COMBAT.UnseenTarget":"Objetivo fuera de vista","FLBR.ROLLER.MaxPush":"Forzajes máximos","FLBR.ROLLER.ModifierActive":"Activo","FLBR.ROLLER.ResolveTest":"Prueba de pérdida de Resiliencia Permanente","FLBR.ROLLER.ResolveTestHint":"Cuando quedas incapacitado por estrés, debes lanzar un dado base correspondiente a tu índice de Empatía. Si sacas un uno, 1 punto de Resiliencia se pierde de manera permanente, lo cual implica que tu Resiliencia queda reducida en una unidad.","FLBR.ROLLER.ResolveTestFailed":"{name} falló la tirada de Empatía y ha perdido 1 punto de Resiliencia de forma permanente.","FLBR.SignatureItemUsedCheckboxHint":"¿Usado?","FLBR.WEAPON_RANGE.Engaged":"Adyacente","FLBR.WEAPON_RANGE.Long":"Largo","FLBR.YouAreBroken":"¡Has quedado Incapacitado!","FLBR.ActorSheet.NotifWrongItemType":"¡El personaje {actor} no puede llevar objetos de tipo {type}!","YZUR.CHAT.ROLL.CancelPush":"Aceptar","YZUR.DIETERMS.D6BladeRunnerDie":"D6","TYPES.Actor.vehicle":"Vehículo","ACTOR.SubtypePc":"Personaje Jugador","TYPES.Item.armor":"Armadura","TYPES.Item.explosive":"Explosivo","SETTINGS.BLADE_RUNNER.AutomaticApplyDamageName":"Infligir Daño Automáticamente","SETTINGS.BLADE_RUNNER.AutomaticApplyDamageHint":"Indica si se inflige daño automáticamente o no a las miniaturas seleccionadas.","SETTINGS.BLADE_RUNNER.AutomaticArmorRollHint":"Indica si se realizarán tiradas de Armadura automáticamente.","SETTINGS.BLADE_RUNNER.OpenFirstWeaponAttackName":"Desplegar primer ataque del arma","SETTINGS.BLADE_RUNNER.OpenFirstWeaponAttackHint":"Indica si se desplegará el primer ataque de un arma al abrir su ficha de objeto.","WARNING.ApplyDamageNoNature":"Infligir daño de Forzaje: ¡Condición del Personaje no encontrada!","WARNING.ApplyDamageNoCapacityKey":"Infligir Daño de Forzaje: ¡No se ha encontrado relación de Condición/Capacidad!","WARNING.NoItemAction":"¡No se ha definido ninguna acción en este objeto!","WARNING.NoItemUnit":"No se puede usar {name}: ¡No te quedan unidades!"} \ No newline at end of file diff --git a/lang/fr.json b/lang/fr.json new file mode 100644 index 0000000..aad26a6 --- /dev/null +++ b/lang/fr.json @@ -0,0 +1 @@ +{"FLBR.COMBAT_ACTION.MeleeAttack":"Attaque de mêlée","FLBR.COMBAT_ACTION.BreakFree":"Se libérer","FLBR.COMBAT_ACTION_HINT.Sprint":"Vous pouvez effectuer des mouvements supplémentaires au cours d'un même round en utilisant l'action courir. Cela comptera alors comme votre action pour le round, ce qui signifie que vous ne pouvez effectuer aucune autre action. Faites un jet de MOBILITÉ – pour chaque S , vous pouvez effectuer un mouvement supplémentaire, en plus de votre mouvement libre. Si vous échouez le jet, vous tombez (avant le deuxième mouvement) et ne pouvez pas vous relever avant votre prochain tour.","FLBR.COMBAT_ACTION_HINT.BreakFree":"Vigueur + Combat rapproché","FLBR.COMBAT_ACTION_HINT.CarefulAim":"Si vous prenez le temps de bien viser avant d'appuyer sur la gâchette, vous bénéficez d'un avantage sur votre jet d'attaque. Cependant, viser est une action en soi. Vous ne pouvez pas viser et tirer dans le même round. Si vous faites autre chose que tirer avec votre arme après avoir visé, ou si vous êtes touché par une attaque, vous perdez l'effet de la visée et vous devez dépenser une autre action pour viser.","FLBR.COMBAT_ACTION_HINT.Manipulate":"Empathie + Manipulation","FLBR.COMBAT_ACTION_HINT.Grapple":"Si vous agrippez votre adversaire en tant qu'attaque spéciale en combat rapproché, vous et votre adversaire tombez au sol. L'adversaire lâche toute arme qu'il tenait et ne peut plus bouger. La seule action qu'il peut effectuer est une tentative de se libérer – qui est une action qui réussit si l'adversaire remporte un jet opposé de COMBAT RAPPROCHÉ contre vous. Pendant que vous agrippez votre adversaire, la seule action que vous pouvez effectuer (jusqu'à ce que vous le libériez) est une attaque de lutte. Cela fonctionne comme une attaque à mains nues, mais votre cible est considérée comme étant attachée et ne peut donc pas se défendre.","FLBR.BUTTON.ToChat":"Envoyer l'objet dans le chat","FLBR.CHAT_ACTION.ApplyDamage":"Appliquer des dégats aux jetons ciblés","FLBR.DIALOG.AssignSuccessesHint":"Choisissez un nombre de succès à dépenser contre {name}.","FLBR.DIALOG.ChooseAction":"Choisir l'action","FLBR.HEADER.Armor":"Armure","FLBR.HEADER.Attacks":"Modes d'attaque","FLBR.HEADER.PromotionPoints":"Points de promotion","FLBR.SHEET_CONFIG.ManeuverabilityHint":"Définit la maniabilité du véhicule conduit par ce personnage, le cas échéant.","FLBR.SHEET_CONFIG.UnlimitedPushHint":"Si votre MJ autorise un nombre illimité de jets poussés. Cochez ce paramètre pour afficher un bouton poussoir sur presque tous les jets.","ACTOR.SubtypeNpc":"Personnage non-joueur","FLBR.TAB.Features":"Options","FLBR.ItemCost":"Coût","FLBR.ItemNoAttack":"Aucun mode d'attaque défini.","FLBR.ItemQuantity":"Quantité","FLBR.MACRO.GetActorTitle":"Choisir un acteur","FLBR.MACRO.GetActorHint":"Vous avez sélectionné plusieurs jetons. Veuillez en choisir un.","FLBR.MACRO.MultipleItems":"Votre acteur contrôlé {actor} a plus d'un élément avec le nom {item}. Le premier élément correspondant sera choisi.","FLBR.MACRO.NoItem":"L'acteur {actor} que vous contrôlez ne possède pas d'objet nommé {item}.","FLBR.Nature":"Nature","FLBR.NatureHint":"Nature du personnage (MJ seulement)","FLBR.RANGED_COMBAT.OutOfRange":"Hors de portée","FLBR.ROLLER.MaxPush":"Poussée max.","FLBR.ROLLER.ModifierName":"Nom","FLBR.ROLLER.ResolveTestHint":"Lorsque vous êtes brisé par le stress, vous devez lancer un dé de base correspondant à votre valeur d'Empathie. Si vous obtenez un 1, un point de Détermination est définitivement perdu, ce qui signifie que votre Détermination maximale est réduite d'un cran.","FLBR.WEAPON_RANGE.Extreme":"Extrême","ACTOR.SubtypePc":"Personnage joueur","FLBR.ATTRIBUTE.MVR":"Manœuvrabilité","FLBR.SKILL.Force":"Force","FLBR.SKILL.Firearms":"Armes à feu","FLBR.SKILL.Tech":"Technologie","FLBR.SKILL.Connections":"Connexions","FLBR.ARCHETYPE.CITY_SPEAKER":"Orateur de la ville","FLBR.ARCHETYPE.ENFORCER":"Fracasseur","FLBR.ARCHETYPE.FIXER":"Réparateur","FLBR.COMBAT_ACTION.Sprint":"Courir","FLBR.COMBAT_ACTION.Crawl":"Ramper","FLBR.ARCHETYPE.SKIMMER":"Ecumeur","FLBR.COMBAT_ACTION.UnarmedAttack":"Attaque à mains nues","FLBR.COMBAT_ACTION.Grapple":"Agripper","FLBR.COMBAT_ACTION.ShootFirearm":"Tirer","FLBR.COMBAT_ACTION.CarefulAim":"Viser","FLBR.COMBAT_ACTION.ThrowWeapon":"Jeter arme","FLBR.COMBAT_ACTION.FirstAid":"Soigner","FLBR.COMBAT_ACTION.Manipulate":"Manipuler","FLBR.COMBAT_ACTION.UseItem":"Utiliser un objet","FLBR.COMBAT_ACTION_HINT.Crawl":"Si vous devez vous déplacer tout en étant couché, vous devez ramper. Cela fonctionne comme un mouvement mais compte comme votre action pour le round. Vous pouvez vous relever à la fin de votre rampement (action libre).","FLBR.COMBAT_ACTION_HINT.UnarmedAttack":"Vigueur + Combat rapproché","FLBR.COMBAT_ACTION_HINT.MeleeAttack":"Vigueur + Combat rapproché","FLBR.COMBAT_ACTION_HINT.ShootFirearm":"Agilité + Armes à feu","FLBR.COMBAT_ACTION_HINT.ThrowWeapon":"Agilité + Armes à feu","FLBR.COMBAT_ACTION_HINT.FirstAid":"Intelligence + Soins médicaux","FLBR.BUTTON.Delete":"Supprimer un objet","FLBR.COMBAT_ACTION_HINT.UseItem":"Utiliser un objet","FLBR.BUTTON.Edit":"Modifier un objet","FLBR.DIALOG.AssignSuccesses":"Attribuer le(s) succès","FLBR.DIALOG.ChooseAttack":"Choisir l'attaque","FLBR.HEADER.Actions":"Actions","FLBR.HEADER.Appearance":"Apparence","FLBR.HEADER.AttributesAndSkills":"Attributs & Compétences","FLBR.HEADER.Augments":"Augmentations","FLBR.HEADER.ChinyenPoints":"Points Chinyen","FLBR.HEADER.DieSize":"Base","FLBR.HEADER.FavoredGear":"Équipement favori","FLBR.HEADER.Health":"Santé","FLBR.HEADER.Home":"Domicile","FLBR.HEADER.HumanityPoints":"Points d'humanité","FLBR.HEADER.Injuries":"Blessures","FLBR.HEADER.Inventory":"Inventaire","FLBR.HEADER.Notes":"Notes","FLBR.HEADER.Modifiers":"Modificateurs","FLBR.HEADER.Resolve":"Détermination","FLBR.HEADER.Specialties":"Spécialités","FLBR.HEADER.KeyMemory":"Souvenir clé","FLBR.HEADER.KeyRelationship":"Relation clé","FLBR.HEADER.Rating":"Rang","FLBR.HEADER.YearsOnTheForce":"Années dans la Force","FLBR.HEADER.Weapons":"Armes","FLBR.SHEET_CONFIG.CharacterSubtype":"Sous-type","FLBR.SHEET_CONFIG.CharacterSubtypeHint":"Choisissez s'il s'agit d'un PJ ou d'un PNJ. Cette dernière option fournit une feuille de personnage allégée et des fonctionnalités supplémentaires permettant au MJ de gérer plus facilement les PNJ.","FLBR.SHEET_CONFIG.HealthMaxModifier":"Modificateur de santé max","FLBR.SHEET_CONFIG.HealthMaxModifierHint":"Modifie le maximum de la capacité de santé (MJ seulement).","FLBR.SHEET_CONFIG.HealthPermanentLoss":"Perte permanente de santé","FLBR.SHEET_CONFIG.HealthPermanentLossHint":"Définit la quantité de santé perdue de façon permanente (réservé aux MJ).","FLBR.SHEET_CONFIG.ResolveMaxModifier":"Modificateur de détermination max","FLBR.SHEET_CONFIG.ResolveMaxModifierHint":"Modifie le maximum de la capacité de résolution (MJ uniquement).","FLBR.SHEET_CONFIG.ResolvePermanentLoss":"Perte permanente de détermination","FLBR.SHEET_CONFIG.ResolvePermanentLossHint":"Définit la quantité de résolution perdue de façon permanente (réservé aux MJ).","FLBR.SHEET_CONFIG.Maneuverability":"Manœuvrabilité du véhicule","FLBR.SHEET_CONFIG.UnlimitedPush":"Permettre un nombre illimité de jets poussés","FLBR.TAB.Actions":"Actions","FLBR.TAB.Bio":"Biographie","FLBR.TAB.Combat":"Combat","FLBR.TAB.Info":"Info","FLBR.TAB.Inventory":"Inventaire","FLBR.TAB.Mods":"Mods","FLBR.TAB.Stats":"Stats","FLBR.D":"D","FLBR.NO_VALUE":"–","FLBR.Advantage":"Avantage","FLBR.Blast":"Explosion","FLBR.BlastPower":"Puissance de l'explosion","FLBR.BrokenByDamage":"Brisé par des dégâts","FLBR.Broken":"Brisé","FLBR.BrokenByStress":"Brisé par le stress","FLBR.Cancel":"Annuler","FLBR.CHAT.DamageDealt":"{name} subit {amount} dégâts.","FLBR.CHAT.NoDamageDealt":"Aucun dégat n'a été infligé à {name}.","FLBR.CHAT.InitialDamageDealt":"L'attaque avait une quantité initiale de {damage} dégâts.","FLBR.CHAT.ArmorDeflectedDamage":"L'armure a dévié {damage} dégats.","FLBR.CHAT.ArmorDeflectedAllDamage":"L'armure a dévié tous les dégâts.","FLBR.CHAT.ArmorNoEffect":"L'armure n'a eu aucun effet.","FLBR.CreateNewItem":"Créer un nouvel objet {type}","FLBR.Crit":"Crit","FLBR.Disadvantage":"Désavantage","FLBR.InjuryCategory":"Catégorie","FLBR.InjuryDeathSave":"Sauvegarde contre la mort","FLBR.InjuryHealing":"Guérison","FLBR.InjuryLethal":"Létal","FLBR.InjuryPermanent":"Permanent","FLBR.InventoryNoItem":"Vide.","FLBR.ItemAddAction":"Ajouter une action","FLBR.ItemAddAttack":"Ajouter une attaque","FLBR.ItemAddModifier":"Ajouter un modificateur de jet","FLBR.ItemActionAttribute":"Attribut","FLBR.ItemActionName":"Nom","FLBR.ItemActionSkill":"Compétence","FLBR.ItemActionType":"Type","FLBR.ItemActionTypeRollSkill":"Jet de compétence","FLBR.ItemActionTypeRunMacro":"Exécuter une macro","FLBR.ItemAttackName":"Nom","FLBR.ItemArmor":"Armure","FLBR.ITEM_AVAILABILITY.Incidental":"Subsidiaire","FLBR.ITEM_AVAILABILITY.Luxury":"Luxe","FLBR.ITEM_AVAILABILITY.Premium":"Premium","FLBR.ITEM_AVAILABILITY.Rare":"Rare","FLBR.ITEM_AVAILABILITY.Standard":"Standard","FLBR.ItemAvailability":"Disponibilité","FLBR.ItemConsumable":"Consommable","FLBR.ItemConsumeNotif":"Consommé {qty} unité de {name}","FLBR.ItemDeleteAction":"Supprimer l'action","FLBR.ItemDeleteAttack":"Supprimer l'attaque","FLBR.ItemDeleteModifier":"Supprimer le modificateur","FLBR.ItemNewAction":"Nouvelle action","FLBR.ItemNewAttack":"Nouvelle attaque","FLBR.ItemNoModifier":"Pas de modificateur.","FLBR.ItemModifier":"Modificateur","FLBR.ItemRollable":"xxx","FLBR.ItemSpecial":"Spécial","FLBR.MACRO.NoActor":"L'acteur {actor} que vous contrôlez n'a pas été trouvé.","FLBR.MACRO.RollAction":"Action {action}","FLBR.MACRO.RollStat":"Lancer {stat}","FLBR.NewItem":"Nouveau {type}","FLBR.OK":"OK","FLBR.Range":"Portée","FLBR.RANGED_COMBAT.SmallTarget":"Petite cible","FLBR.RANGED_COMBAT.LargeTarget":"Grande cible","FLBR.RANGED_COMBAT.TargetBehindCover":"Cible à couvert","FLBR.RANGED_COMBAT.CarefulAim":"Viser","FLBR.RANGED_COMBAT.UnseenTarget":"Cible dissimulée","FLBR.ROLLER.AddDie":"Ajouter un dé","FLBR.ROLLER.ModifierActive":"Actif","FLBR.ROLLER.ModifierValue":"Valeur","FLBR.ROLLER.TotalModifier":"Total : {value}","FLBR.ROLLER.ResolveTestFailed":"{name} a raté son jet d'Empathie et perd définitivement 1 point de détermination.","FLBR.ROLLER.ResolveTest":"Test pour la perte permanente de détermination","FLBR.ROLLER.RollButton":"Lancer","FLBR.ROLLER.RollWithAdvantageButton":"Avantage","FLBR.ROLLER.RollWithDisadvantageButton":"Désavantage","FLBR.ROLLER.UnlimitedPush":"Illimité","FLBR.SignatureItemUsedCheckboxHint":"Utilisé ?","TYPES.Item.generic":"Générique","TYPES.Item.upgrade":"Augmentation","TYPES.Item.armor":"Armure","TYPES.Item.weapon":"Arme","TYPES.Item.explosive":"Explosif","TYPES.Item.specialty":"Spécialité","FLBR.WeaponDamage":"Dégâts","FLBR.WeaponCrit":"Dé Crit.","FLBR.WeaponDamageType":"Type de dégats","FLBR.WEAPON_DAMAGE_TYPE.None":"–","FLBR.WEAPON_DAMAGE_TYPE.Crushing":"Ecrasant","FLBR.WEAPON_DAMAGE_TYPE.Piercing":"Perçant","FLBR.WEAPON_DAMAGE_TYPE.Stress":"Stress","FLBR.WeaponRange":"Portée","FLBR.WEAPON_RANGE.Engaged":"Engagé","FLBR.WEAPON_RANGE.Short":"Court","FLBR.WEAPON_RANGE.Medium":"Moyen","FLBR.WEAPON_RANGE.Long":"Long","FLBR.YouAreBroken":"Tu es brisé !","FLBR.ActorSheet.NotifWrongItemType":"L'acteur {actor} ne peut pas contenir d'objets de type {type} !","YZUR.CHAT.ROLL.Infos":"Infos","YZUR.CHAT.ROLL.Damage":"Dégats de base","YZUR.CHAT.ROLL.Push":"Pousser","YZUR.CHAT.ROLL.CancelPush":"Accepter","YZUR.DIETERMS.D6BladeRunnerDie":"D6","YZUR.DIETERMS.D8BladeRunnerDie":"D8","YZUR.DIETERMS.D10BladeRunnerDie":"D10","YZUR.DIETERMS.D12BladeRunnerDie":"D12","TYPES.Actor.character":"Personnage","TYPES.Actor.vehicle":"Véhicule","FLBR.Attributes":"Attributs","FLBR.ATTRIBUTE.STR":"Vigueur","FLBR.ATTRIBUTE.AGI":"Agilité","FLBR.ATTRIBUTE.INT":"Intelligence","FLBR.ATTRIBUTE.EMP":"Empathie","FLBR.Skills":"Compétences","FLBR.SKILL.CloseCombat":"Combat rapproché","FLBR.SKILL.Stamina":"Endurance","FLBR.SKILL.Stealth":"Furtivité","FLBR.SKILL.MedicalAid":"Soins médicaux","FLBR.SKILL.Observation":"Observation","FLBR.SKILL.Driving":"Conduite","FLBR.NATURE.Human":"Humain","FLBR.SKILL.Insight":"Intuition","FLBR.SKILL.Manipulation":"Manipulation","FLBR.ARCHETYPE.ANALYST":"Analyste","FLBR.ARCHETYPE.INSPECTOR":"Inspecteur","FLBR.SKILL.Mobility":"Mobilité","FLBR.NATURE.Replicant":"Réplicant","FLBR.ARCHETYPE.DOXIE":"Doxie","FLBR.BUTTON.Roll":"Lancer les dés pour l'objet","FLBR.HEADER.SignatureItem":"Objet signature","FLBR.SHEET_HEADER.ArmorRoll":"Jet d'armure","FLBR.SHEET_HEADER.BlastPowerRoll":"Jet de puissance de l'explosion","FLBR.SHEET_HEADER.GenericRoll":"Jet générique","FLBR.SHEET_HEADER.ItemPost":"Déposer objet","FLBR.SHEET_HEADER.ItemRoll":"Jet d'objet","TYPES.Item.injury":"Blessure","GAME.Paused":"Investigation en pause","SETTINGS.BLADE_RUNNER.AutomaticApplyDamageName":"Application automatique des dégats","SETTINGS.BLADE_RUNNER.AutomaticApplyDamageHint":"Appliquer automatiquement les dégâts des jets aux jetons ciblés et aux passagers des véhicules.","SETTINGS.BLADE_RUNNER.AutomaticArmorRollName":"Jet d'armure automatique","SETTINGS.BLADE_RUNNER.AutomaticArmorRollHint":"Si l'on veut que le jet d'armure soit fait automatiquement.","SETTINGS.BLADE_RUNNER.OpenFirstWeaponAttackName":"Ouvrir la première attaque","SETTINGS.BLADE_RUNNER.OpenFirstWeaponAttackHint":"Ouvrir l'accordéon de la première attaque lorsque la feuille de l'arme est affichée.","WARNING.ApplyDamageNoNature":"Application des dégâts de poussée : Nature de l'acteur pas trouvée !","WARNING.ApplyDamageNoCapacityKey":"Application des dégâts de poussée : Relation Nature-Capacité introuvable !","WARNING.ApplyDamageNoActorCapacity":"Application des dégâts de poussée : Capacité de l'acteur pas trouvée !","WARNING.NoDiceInput":"Aucun dé à lancer !","WARNING.NoItemAction":"Aucune action définie !","WARNING.ItemActionMacroNotFound":"Macro \"{macro}\" pas trouvée !","WARNING.NoItemUnit":"Impossible d'utiliser {name} : pas d'unité restante !","FLBR.ROLLER.SelectDiceToPush":"Sélectionnez les dés à relancer","FLBR.Capacities":"Capacités","FLBR.BUTTON.Mount":"Monter une arme","FLBR.BUTTON.Unmount":"Démonter une arme","FLBR.HEADER.Hull":"Coque","FLBR.ACTIVE_EFFECT.Delete":"Supprimer l'effet","FLBR.ACTIVE_EFFECT.Edit":"Editer l'effet","FLBR.ACTIVE_EFFECT.Empty":"Aucun effet.","FLBR.ACTIVE_EFFECT.New":"Nouvel effet","FLBR.ACTIVE_EFFECT.Toggle":"Basculer l'effet","FLBR.CHAT.CritInflicted":"{name} reçoit la blessure critique {crit}.","FLBR.COMBAT.Combat":"Combat","FLBR.COMBAT.CannotBeDamaged":"Ne peut pas être endommagé","FLBR.COMBAT.DrawSize":"Taille de pioche","FLBR.COMBAT.SkippedDefender":"Défenseur {name} ignoré","FLBR.CRIT.CriticalInjury":"Blessure critique","FLBR.CRIT.Crit":"Crit","FLBR.CRIT.ChooseCrit":"Choisir une blessure critique","FLBR.CRIT.ChooseVehicleDamage":"Choisir un dégât critique pour le véhicule","FLBR.CRIT.ChooseActor":"Choisir un acteur qui recevra une blessure critique.","FLBR.CRIT.DrawCrit":"Tirer une blessure critique","FLBR.CRIT.DrawVehicleDamage":"Tirer un dégât critique de véhicule","FLBR.CustomFormula":"Formule personnalisée","FLBR.Formula":"Formule","FLBR.ItemMounted":"Monté","FLBR.ROLLER.RollSpecialHint":"Lancer : {name} ?","FLBR.SomeoneIsSomething":"{name} est {status}","FLBR.VEHICLE.Action.Explode":"Exploser","FLBR.VEHICLE.Action.Ramming":"Percuter","FLBR.VEHICLE.Action.Repair":"Réparer","FLBR.VEHICLE.Action.Speeding":"Accélérer","FLBR.VEHICLE.Altitude":"Altitude","FLBR.VEHICLE.AddSeat":"Augmenter le nombre de passagers (MJ seulement)","FLBR.VEHICLE.ChoosePassenger":"Choisir un passager","FLBR.VEHICLE.ChoosePassengerHint":"Choisir l'un des passagers.","FLBR.VEHICLE.Components":"Composants","FLBR.VEHICLE.CrashDamageFlavor":"Dégâts infligés à l'équipage lors du crash du véhicule","FLBR.VEHICLE.Action.MassiveCrash":"Crash total","FLBR.VEHICLE.Action.ExplodeHint":"Le véhicule explose dans une gerbe de flammes. Chaque passager à l'intérieur subit une explosion de puissance B, et le véhicule est détruit au-delà de toute réparation.","FLBR.VEHICLE.Action.Crash":"Crash","FLBR.VEHICLE.Action.CrashHint":"Si un Spinner est démoli, il se crash violemment au sol. Toute personne à l'intérieur subit D3 points de dégâts de chute plus 1 pour chaque zone d'altitude, mitigé par l'armure du véhicule. Vous pouvez également vous préparer au coup en lançant MOBILITÉ– chaque S obtenu réduit les dégâts de 1.","FLBR.VEHICLE.Action.SpeedingHint":"Similaire à l'action de courir à pied, vous pouvez utiliser l'action d'accélérer pour effectuer des mouvements supplémentaires avec le véhicule lors d'un même round.","FLBR.VEHICLE.Action.CrashDialog":"Crasher le véhicule et infliger {damage} dégâts de chute à chaque passager, mitigé par l'armure du véhicule et un lancer de MOBILITÉ (ne compte pas comme une action).","FLBR.VEHICLE.Crew":"Equipage","FLBR.VEHICLE.DecreaseHull":"Diminuer la coque max.","FLBR.VEHICLE.DeleteSeat":"Diminuer le nombre de passagers","FLBR.VEHICLE.EditPassenger":"Editer le passager","FLBR.VEHICLE.Engine":"Moteur","FLBR.VEHICLE.IncreaseHull":"Augmenter la coque max.","FLBR.VEHICLE.MountedWeapons":"Armes montées","FLBR.VEHICLE.NotifCrewFull":"Plus de place libre !","FLBR.VEHICLE.NotifDropCrewLoot":"Mettez vos ordures dans le coffre à la place !","FLBR.VEHICLE.NotifPassengerAlreadyPresent":"Le passager est déjà présent !","FLBR.VEHICLE.RemovePassenger":"Retirer le passager","FLBR.Wrecked":"Démoli","YZEC.CombatTracker.FastAction":"A bougé","YZEC.CombatTracker.SlowAction":"A acté","ACTOR.TypeLoot":"Pile d'objets","SETTINGS.BLADE_RUNNER.CrashTableName":"Table des dégâts de véhicule","SETTINGS.BLADE_RUNNER.CrashTableHint":"ID ou nom de la table des dégâts critiques pour véhicule.","SETTINGS.BLADE_RUNNER.CrushingTableName":"Table des blessures écrasantes","SETTINGS.BLADE_RUNNER.CrushingTableHint":"ID ou nom de la table des blessures critiques écrasantes.","SETTINGS.BLADE_RUNNER.PiercingTableName":"Table des blessures perçantes","SETTINGS.BLADE_RUNNER.PiercingTableHint":"ID ou nom de la table des blessures critiques perçantes.","SETTINGS.BLADE_RUNNER.EditNaturePermissionName":"Autoriser le changement de nature","SETTINGS.BLADE_RUNNER.EditNaturePermissionHint":"Autoriser le joueur à éditer sa nature dans sa fiche de personnage.","SETTINGS.BLADE_RUNNER.UpdateActorManeuverabilityOnCrewHint":"Mettre à jour la manœuvrabilité de l'acteur lorsque qu'il est ajouté à un véhicule.","SETTINGS.BLADE_RUNNER.UpdateActorManeuverabilityOnUncrewHint":"Mettre à jour la manœuvrabilité de l'acteur quand il est retiré du véhicule.","SETTINGS.BLADE_RUNNER.UseActiveEffectsName":"Utiliser les effets","SETTINGS.BLADE_RUNNER.DoNotUseHandwrittenFontName":"Ne pas utiliser de police manuscrite","SETTINGS.BLADE_RUNNER.DoNotUseHandwrittenFontHint":"Désactive l'utilisation de la police manuscrite \"Caveat\" dans les feuilles de personnage.","SETTINGS.BLADE_RUNNER.UpdateActorManeuverabilityOnCrewName":"Mise à jour manœuvrabilité quand équipe","SETTINGS.BLADE_RUNNER.UpdateActorManeuverabilityOnUncrewName":"Mise à jour manœuvrabilité quand déséquipe","FLBR.ACTIVE_EFFECT.Add":"Ajouter un effet","FLBR.ACTIVE_EFFECT.InActorDisclaimer":"Note : les effets ne peuvent pas être modifiés dans les objets qui sont embarqués (limitation de Foundry).","FLBR.CRIT.CritButtonTooltip":"Tirer une blessure critique pour le personnage du joueur ou un jeton sélectionné","FLBR.VEHICLE.CreateActor":"Créer un acteur ?","FLBR.VEHICLE.CreateActorHint":"Voulez-vous dupliquer l'acteur {name} avant de l'ajouter à l'équipage ?","FLBR.VEHICLE.NotifDropCrewVehicle":"Les inceptions de véhicules ne sont pas autorisées !","SETTINGS.BLADE_RUNNER.UseActiveEffectsHint":"Montrer un champ additionnel dans les feuilles pour gérer les effets (pour utilisateur avancé).","FLBR.VEHICLE.Action.RepairHint":"Réparer les dégâts d'un véhicule requiert un ou plusieurs lancers de TECHNOLOGIE. Un seul lancer peut être tenté par Shift. Une seule personne peut tenter la réparation, mais les autres peuvent aider. Chaque S obtenu retire 1 point de dégât. Si le véhicule était démoli, il ne l'est plus dès lors qu'un point de dégâts a été réparé. Si le moteur ou une arme montée a été endommagé lors d'un coup critique, le réparer requiert typiquement un lancer de TECHNOLOGIE, en supplément des lancers pour réparer les dégâts de la coque. Ce travail dure un Shift.","FLBR.VEHICLE.Action.RammingHint":"La plupart des véhicules peuvent être utilisés comme une arme – ex. rouler sur votre cible. Cela compte comme une attaque au combat rapproché mais lancez CONDUITE au lieu de COMBAT RAPPROCHÉ. Ce lancer peut être combiné avec l'action d'accélérer, mais dans ce cas le lancer de CONDUITE devient un jet opposé, signifiant que l'efficacité de l'accélération peut être réduite. Les dégâts de base de l'attaque sont égaux à la valeur de la coque du véhicule divisée par deux, arrondie au supérieur. Percuter avec un véhicule inflige des dégâts écrasants avec un dé critique de taille égale à la valeur de la coque, arrondie au dé le plus proche de taille supérieure, jusqu'au maximum D12 – ex. un véhicule avec une coque 9 a un dé critique de D10.","TYPES.Actor.loot":"Pile d'objets","FLBR.SHEET_CONFIG.Title":"Configuration de la feuille","FLBR.SHEET_CONFIG.CharacterConfiguration":"Configuration du personnage","FLBR.SHEET_CONFIG.Document":"Document","FLBR.SHEET_CONFIG.Defaults":"Valeurs par défaut"} \ No newline at end of file diff --git a/lang/ko.json b/lang/ko.json new file mode 100644 index 0000000..5ef5439 --- /dev/null +++ b/lang/ko.json @@ -0,0 +1 @@ +{"FLBR.ATTRIBUTE.STR":"힘","FLBR.ATTRIBUTE.AGI":"민첩","FLBR.ATTRIBUTE.INT":"지능","FLBR.ATTRIBUTE.EMP":"공감","FLBR.Skills":"기술","FLBR.SKILL.CloseCombat":"격투","FLBR.SKILL.Force":"완력","FLBR.SKILL.Stamina":"강단","FLBR.SKILL.Firearms":"총기","FLBR.SKILL.Mobility":"운동","FLBR.SKILL.Stealth":"잠행","FLBR.SKILL.MedicalAid":"의료","FLBR.SKILL.Observation":"관찰","FLBR.SKILL.Tech":"공학","FLBR.SKILL.Connections":"연줄","FLBR.SKILL.Insight":"조작","FLBR.SKILL.Driving":"운전","FLBR.NATURE.Human":"인간","FLBR.NATURE.Replicant":"레플리컨트","FLBR.ARCHETYPE.ANALYST":"분석가","FLBR.ARCHETYPE.CITY_SPEAKER":"정보통","FLBR.ARCHETYPE.DOXIE":"화류인","FLBR.ARCHETYPE.ENFORCER":"집행자","FLBR.ARCHETYPE.FIXER":"해결사","FLBR.ARCHETYPE.INSPECTOR":"보안관","FLBR.ARCHETYPE.SKIMMER":"돈받이","FLBR.COMBAT_ACTION.Sprint":"전력 질주","FLBR.COMBAT_ACTION.UnarmedAttack":"비무장 공격","FLBR.COMBAT_ACTION.MeleeAttack":"근접 공격","FLBR.COMBAT_ACTION.Grapple":"붙잡기","FLBR.COMBAT_ACTION.BreakFree":"풀려나기","FLBR.COMBAT_ACTION.ShootFirearm":"총기 발사","FLBR.COMBAT_ACTION.CarefulAim":"조심스러운 조준","FLBR.COMBAT_ACTION.FirstAid":"응급 치료","FLBR.COMBAT_ACTION.Manipulate":"조작하기","FLBR.COMBAT_ACTION.UseItem":"물건 사용","FLBR.COMBAT_ACTION_HINT.Crawl":"엎드린 상태로 이동해야 한다면, 기어야 합니다. 기어가기는 이동과 같게 진행하지만, 그 라운드에서 여러분이 수행하는 행동으로 취급됩니다. 기어가기를 마칠 때 무료 행동으로 일어날 수 있습니다.","FLBR.COMBAT_ACTION_HINT.UnarmedAttack":"힘 + 근접 공격","FLBR.COMBAT_ACTION_HINT.MeleeAttack":"힘+ 근접 공격","FLBR.COMBAT_ACTION_HINT.BreakFree":"힘 + 격투","FLBR.COMBAT_ACTION_HINT.ThrowWeapon":"민첩 + 총기","FLBR.COMBAT_ACTION_HINT.FirstAid":"지능 + 의료","FLBR.COMBAT_ACTION_HINT.Manipulate":"공감 + 조작","FLBR.COMBAT_ACTION_HINT.UseItem":"사용하기","FLBR.BUTTON.Delete":"지우기","FLBR.BUTTON.Edit":"수정하기","FLBR.BUTTON.Roll":"굴리기","FLBR.BUTTON.ToChat":"대화창으로 보내기","FLBR.CHAT_ACTION.ApplyDamage":"목표 토큰에 피해 적용","FLBR.DIALOG.AssignSuccesses":"성공값 할당","FLBR.DIALOG.AssignSuccessesHint":"{name}에 성공값을 얼마나 사용할지 선택하세요.","FLBR.DIALOG.ChooseAction":"행동 선택","FLBR.DIALOG.ChooseAttack":"공격 선택","FLBR.HEADER.Actions":"행동","FLBR.HEADER.Appearance":"외모","FLBR.HEADER.Armor":"장갑","FLBR.HEADER.Attacks":"공격 모드","FLBR.HEADER.AttributesAndSkills":"특성과 기술","FLBR.HEADER.Augments":"강화","FLBR.HEADER.ChinyenPoints":"친엔 점수","FLBR.HEADER.DieSize":"기본 주사위","FLBR.HEADER.FavoredGear":"선호 장비","FLBR.HEADER.Health":"체력","FLBR.HEADER.Home":"집","FLBR.HEADER.HumanityPoints":"인간성 점수","FLBR.HEADER.Injuries":"부상","FLBR.HEADER.Inventory":"소지품","FLBR.HEADER.Notes":"상세","FLBR.HEADER.Modifiers":"변경자","FLBR.HEADER.PromotionPoints":"진급 점수","FLBR.HEADER.Resolve":"결의","FLBR.HEADER.SignatureItem":"나만의 물건","FLBR.HEADER.Specialties":"전문화","FLBR.HEADER.KeyMemory":"핵심 기억","FLBR.HEADER.KeyRelationship":"핵심 관계","FLBR.HEADER.Rating":"등급","FLBR.HEADER.YearsOnTheForce":"근속 연수","FLBR.HEADER.Weapons":"무기","FLBR.SHEET_CONFIG.CharacterSubtype":"하위 종류","FLBR.SHEET_CONFIG.HealthMaxModifier":"최대 체력 변경","FLBR.SHEET_CONFIG.HealthMaxModifierHint":"최대 체력 한도를 변경합니다 (GM 전용).","FLBR.SHEET_CONFIG.HealthPermanentLoss":"체력 영구 손실","FLBR.SHEET_CONFIG.HealthPermanentLossHint":"영구히 잃는 체력의 양을 정합니다(GM 전용).","FLBR.SHEET_CONFIG.ResolveMaxModifier":"최대 결의 변경","FLBR.SHEET_CONFIG.ResolveMaxModifierHint":"최대 결의 한도를 변경합니다(GM 전용).","FLBR.SHEET_CONFIG.ResolvePermanentLossHint":"영구히 잃는 결의의 양을 정합니다(GM 전용).","FLBR.SHEET_CONFIG.Maneuverability":"차량 조종성","FLBR.SHEET_CONFIG.UnlimitedPush":"강행 굴림 무제한 허용","FLBR.SHEET_CONFIG.UnlimitedPushHint":"GM이 강행 굴림을 무제한 허용했다면, 여기에 체크해 강행 버튼을 표기합니다.","FLBR.SHEET_HEADER.ArmorRoll":"장갑 굴림","FLBR.SHEET_HEADER.BlastPowerRoll":"폭발 굴림","FLBR.SHEET_HEADER.GenericRoll":"일반 굴림","FLBR.SHEET_HEADER.ItemPost":"물건 올리기","FLBR.SHEET_HEADER.ItemRoll":"물건 굴리기","FLBR.TAB.Actions":"행동","FLBR.TAB.Bio":"배경 이야기","FLBR.TAB.Combat":"전투","FLBR.TAB.Info":"정보","FLBR.TAB.Features":"특징","FLBR.TAB.Mods":"기타 요인","FLBR.TAB.Stats":"수치","FLBR.D":"D","FLBR.Advantage":"이점","FLBR.Capacities":"체력과 결의","FLBR.Blast":"폭발","FLBR.BlastPower":"폭발 강도","FLBR.Broken":"무너짐","FLBR.BrokenByStress":"스트레스로 무너짐","FLBR.Cancel":"취소","FLBR.CHAT.DamageDealt":"{name}이(가) {amount} 피해를 입었습니다.","FLBR.CHAT.InitialDamageDealt":"이 공격은 기본 {damage} 피해를 입힙니다.","FLBR.CHAT.ArmorDeflectedDamage":"장갑이 {damage} 피해를 막아냅니다.","FLBR.CHAT.ArmorDeflectedAllDamage":"장갑이 모든 피해를 막아냅니다.","FLBR.CHAT.ArmorNoEffect":"장갑이 효과가 없습니다.","FLBR.Crit":"치명","FLBR.Disadvantage":"난점","FLBR.InjuryCategory":"종류","FLBR.InjuryDeathSave":"죽음 저항","FLBR.InjuryHealing":"치유","FLBR.InjuryLethal":"치사","FLBR.InjuryPermanent":"영구","FLBR.InventoryNoItem":"비어있음.","FLBR.ItemAddAction":"행동 추가","FLBR.ItemAddAttack":"공격 추가","FLBR.ItemAddModifier":"굴림 변경자 추가","FLBR.ItemActionAttribute":"특성","FLBR.ItemActionSkill":"기술","FLBR.ItemActionType":"행동 종류","FLBR.ItemActionTypeRollSkill":"기술 굴림","FLBR.ItemActionTypeRunMacro":"매크로 사용","FLBR.ItemArmor":"장갑","FLBR.ITEM_AVAILABILITY.Incidental":"잡동사니","FLBR.ITEM_AVAILABILITY.Luxury":"사치품","FLBR.ITEM_AVAILABILITY.Premium":"고급품","FLBR.ITEM_AVAILABILITY.Rare":"희귀품","FLBR.ITEM_AVAILABILITY.Standard":"일반","FLBR.ItemAvailability":"희귀도","FLBR.ItemConsumable":"소모품","FLBR.ItemCost":"비용","FLBR.ItemDeleteAction":"행동 제거","FLBR.ItemDeleteAttack":"공격 제거","FLBR.ItemDeleteModifier":"변경자 제거","FLBR.ItemNewAction":"신규 행동","FLBR.ItemNewAttack":"신규 공격","FLBR.ItemNoAttack":"공격 모드 미설정.","FLBR.ItemNoModifier":"변경자 없음.","FLBR.ItemModifier":"변경자","FLBR.ItemSpecial":"특수","FLBR.MACRO.GetActorTitle":"행위자 선택","FLBR.MACRO.MultipleItems":"당신이 통제하는 {actor}이(가) 같은 {item}을(를) 여러개 가지고 있습니다. 가장 위에 있는 물건이 선택됩니다.","FLBR.MACRO.NoActor":"당신이 통제하는 {actor}을(를) 찾을 수 없습니다.","FLBR.MACRO.RollAction":"{action} 행동","FLBR.MACRO.RollStat":"{stat} 굴림","FLBR.Nature":"본질","FLBR.NatureHint":"캐릭터의 본질 (GM 전용)","FLBR.NewItem":"신규 {type}","FLBR.OK":"네","FLBR.Range":"거리","FLBR.RANGED_COMBAT.OutOfRange":"거리 벗어남","FLBR.RANGED_COMBAT.SmallTarget":"소형 목표","FLBR.RANGED_COMBAT.LargeTarget":"대형 목표","FLBR.RANGED_COMBAT.TargetBehindCover":"엄폐를 취한 목표","FLBR.RANGED_COMBAT.CarefulAim":"조심스러운 조준","FLBR.RANGED_COMBAT.UnseenTarget":"보이지 않는 목표","FLBR.ROLLER.AddDie":"주사위 추가","FLBR.ROLLER.ModifierActive":"활성화","FLBR.ROLLER.ModifierName":"이름","FLBR.ROLLER.ModifierValue":"값","FLBR.ROLLER.TotalModifier":"최종: {value}","FLBR.ROLLER.ResolveTest":"결의 영구 상실 테스트","FLBR.ROLLER.ResolveTestFailed":"{name}이(가) 공감 굴림에 실패해 결의 1점을 영구적으로 잃습니다.","FLBR.ROLLER.RollButton":"굴림","FLBR.ROLLER.RollWithAdvantageButton":"이점","FLBR.ROLLER.RollWithDisadvantageButton":"난점","FLBR.ROLLER.UnlimitedPush":"무제한","FLBR.WeaponDamage":"피해","FLBR.WeaponCrit":"치명 주사위","FLBR.WeaponDamageType":"피해 종류","FLBR.WEAPON_DAMAGE_TYPE.Crushing":"으스러짐","FLBR.WEAPON_DAMAGE_TYPE.Piercing":"관통","FLBR.WEAPON_DAMAGE_TYPE.Stress":"스트레스","FLBR.WeaponRange":"거리","FLBR.WEAPON_RANGE.Engaged":"인접","FLBR.WEAPON_RANGE.Short":"단","FLBR.WEAPON_RANGE.Medium":"중","FLBR.WEAPON_RANGE.Long":"장","FLBR.WEAPON_RANGE.Extreme":"초장","FLBR.YouAreBroken":"무너집니다!","YZUR.CHAT.ROLL.Infos":"정보","YZUR.CHAT.ROLL.Damage":"기본 피해","YZUR.CHAT.ROLL.Push":"강행","YZUR.CHAT.ROLL.CancelPush":"수락","YZUR.DIETERMS.D6BladeRunnerDie":"D6","YZUR.DIETERMS.D8BladeRunnerDie":"D8","YZUR.DIETERMS.D10BladeRunnerDie":"D10","YZUR.DIETERMS.D12BladeRunnerDie":"D12","TYPES.Actor.character":"캐릭터","TYPES.Actor.vehicle":"차량","ACTOR.SubtypePc":"플레이어 캐릭터","ACTOR.SubtypeNpc":"비 플레이어 캐릭터","TYPES.Item.generic":"일반","TYPES.Item.upgrade":"강화","TYPES.Item.armor":"장갑","TYPES.Item.weapon":"무기","TYPES.Item.explosive":"폭발물","TYPES.Item.specialty":"전문화","TYPES.Item.injury":"부상","SETTINGS.BLADE_RUNNER.AutomaticApplyDamageName":"피해 자동 적용","SETTINGS.BLADE_RUNNER.AutomaticArmorRollName":"장갑 자동 굴림","SETTINGS.BLADE_RUNNER.AutomaticArmorRollHint":"장갑 굴림을 자동으로 수행할지를 정합니다.","SETTINGS.BLADE_RUNNER.OpenFirstWeaponAttackName":"첫 무기 공격 펼치기","SETTINGS.BLADE_RUNNER.OpenFirstWeaponAttackHint":"물건 시트가 표시될 때 첫 무기의 상세를 펼칠지를 정합니다.","WARNING.ApplyDamageNoNature":"강행 피해 적용: 행위자 본질이 정해지지 않았습니다!","WARNING.ApplyDamageNoActorCapacity":"강행 피해 적용: 행위자 한계치가 정해지지 않았습니다!","WARNING.NoDiceInput":"굴릴 주사위가 없습니다!","WARNING.NoItemAction":"물건 행동이 정해지지 않았습니다!","WARNING.ItemActionMacroNotFound":"\"{macro}\" 매크로를 찾을 수 없습니다!","FLBR.SignatureItemUsedCheckboxHint":"사용 여부?","FLBR.Attributes":"특성","FLBR.ATTRIBUTE.MVR":"조종성","FLBR.SKILL.Manipulation":"통찰","FLBR.COMBAT_ACTION.Crawl":"기어가기","FLBR.COMBAT_ACTION.ThrowWeapon":"무기 던지기","FLBR.COMBAT_ACTION_HINT.Sprint":"한 라운드에서 전력 질주 행동을 사용하여 이동을 추가로 수행할 수 있습니다. 전력 질주는 그 라운드에서 여러분이 수행하는 행동으로 취급되기에, 다른 행동은 수행할 수 없습니다. 운동 굴림을 수행합니다. 결괏값으로 나온 S마다, 기본 이동에 더해 이동을 한 번씩 더 할 수 있습니다. 굴림이 실패하면, (두 번째 이동을 수행하기 전에) 넘어지고 다음 턴까지 일어날 수 없습니다.","FLBR.COMBAT_ACTION_HINT.Grapple":"근접 전투에서 특수 공격을 사용해 상대를 붙잡으면 여러분과 상대는 모두 바닥에 넘어집니다. 상대는 들고 있던 모든 무기를 떨어트리고 이동할 수 없습니다. 상대가 수행할 수 있는 유일한 행동은 풀려나기 뿐입니다. 이는 행동이고, 여러분을 대상으로 하는 격투 대항 굴림에서 이겨야 성공합니다. 상대를 붙잡고 있는 동안, (놓아주기 전까지) 여러분이 수행할 수 있는 유일한 행동은 붙잡기 공격뿐입니다. 이는 일반적인 비무장 공격으로 수행하지만, 상대는 구속된 상태로 취급되기에 자신을 방어할 수 없습니다.","FLBR.COMBAT_ACTION_HINT.ShootFirearm":"민첩 + 총기","FLBR.COMBAT_ACTION_HINT.CarefulAim":"방아쇠를 당기기 전에 시간을 들여 조준한다면 공격 굴림에 이점을 얻습니다. 하지만, 조준은 그 자체로 행동입니다. 조준과 사격을 같은 라운드에 모두 할 수는 없습니다. 조준한 뒤 사격 이외에 다른 무엇이라도 한다던가 공격으로 타격당한다면, 조준의 효과를 잃어버립니다. 다시 조준하려면 행동을 또 소비해야 합니다.","FLBR.SHEET_CONFIG.CharacterSubtypeHint":"PC/NPC 여부를 표시합니다. NPC로 표시하면 시트가 간략해져 GM이 NPC를 편하게 다룰 수 있습니다.","FLBR.SHEET_CONFIG.ResolvePermanentLoss":"결의 영구 손실","FLBR.SHEET_CONFIG.ManeuverabilityHint":"이 캐릭터가 모는 탈 것의 조종성을 정합니다.","FLBR.TAB.Inventory":"소지품","FLBR.BrokenByDamage":"피해로 무너짐","FLBR.CHAT.NoDamageDealt":"{name}은(는) 피해를 입지 않았습니다.","FLBR.CreateNewItem":"새 {type} 물건을 만듭니다","FLBR.ItemActionName":"행동 이름","FLBR.ItemAttackName":"공격 이름","FLBR.ItemConsumeNotif":"{name}을(를) {qty}개 소모했습니다","FLBR.ItemQuantity":"수량","FLBR.MACRO.GetActorHint":"토큰을 여러개 선택하셨습니다. 하나만 선택해주세요.","FLBR.MACRO.NoItem":"당신이 통제하는 {actor}이(가) {item}을(를) 가지고 있지 않습니다.","FLBR.ROLLER.MaxPush":"최대 강행","FLBR.ROLLER.ResolveTestHint":"스트레스로 인해 무너졌다면, 반드시 공감 등급에 해당하는 기본 주사위 하나를 굴려야 합니다. 만약 결괏값으로 1이 나왔다면, 결의 1점이 영구적으로 사라집니다. 최대 결의가 1점 낮아진다는 뜻입니다.","FLBR.ROLLER.SelectDiceToPush":"강행할 주사위 선택","FLBR.ActorSheet.NotifWrongItemType":"{actor}은(는) {type}을(를) 가질 수 없습니다!","GAME.Paused":"수사 일시 중지","SETTINGS.BLADE_RUNNER.AutomaticApplyDamageHint":"굴림에서 나온 피해를 선택한 토큰에 자동으로 적용할지를 정합니다.","WARNING.ApplyDamageNoCapacityKey":"강행 피해 적용: 본질-한계치가 정해지지 않았습니다!","WARNING.NoItemUnit":"{name} 사용 불가: 모두 사용했습니다!"} \ No newline at end of file diff --git a/lang/pl.json b/lang/pl.json new file mode 100644 index 0000000..777ae4a --- /dev/null +++ b/lang/pl.json @@ -0,0 +1 @@ +{"FLBR.Attributes":"Atrybuty","FLBR.ATTRIBUTE.STR":"Siła","FLBR.ATTRIBUTE.AGI":"Zręczność","FLBR.ATTRIBUTE.INT":"Inteligencja","FLBR.ATTRIBUTE.EMP":"Empatia","FLBR.ATTRIBUTE.MVR":"Manewrowość","FLBR.Skills":"Umiejętności","FLBR.SKILL.CloseCombat":"Bijatyka","FLBR.SKILL.Force":"Impet","FLBR.SKILL.Stamina":"Kondycja","FLBR.SKILL.Firearms":"Celność","FLBR.SKILL.Mobility":"Sprawność","FLBR.SKILL.Stealth":"Dyskrecja","FLBR.SKILL.MedicalAid":"Medycyna","FLBR.SKILL.Observation":"Obserwacja","FLBR.SKILL.Tech":"Technika","FLBR.COMBAT_ACTION.Crawl":"Pełzanie","FLBR.COMBAT_ACTION.UnarmedAttack":"Atak Bez Broni","FLBR.COMBAT_ACTION.MeleeAttack":"Atak Wręcz","FLBR.COMBAT_ACTION.Grapple":"Chwyt","FLBR.COMBAT_ACTION.BreakFree":"Wyrwanie Się","FLBR.COMBAT_ACTION.CarefulAim":"Staranne Celowanie","FLBR.COMBAT_ACTION.ThrowWeapon":"Rzut Bronią","FLBR.COMBAT_ACTION.FirstAid":"Pierwsza Pomoc","FLBR.COMBAT_ACTION.Manipulate":"Manipulacja","FLBR.COMBAT_ACTION.UseItem":"Użyj Przedmiotu","FLBR.COMBAT_ACTION_HINT.UnarmedAttack":"Siła + Bijatyka","FLBR.COMBAT_ACTION_HINT.ThrowWeapon":"Zręczność + Strzelectwo","FLBR.COMBAT_ACTION_HINT.FirstAid":"Inteligencja + Pomoc Medyczna","FLBR.COMBAT_ACTION_HINT.Manipulate":"Empatia + Manipulacja","FLBR.COMBAT_ACTION_HINT.UseItem":"Użyj Przedmiotu","FLBR.DIALOG.AssignSuccesses":"Przydziel Sukces(y)","FLBR.HEADER.Home":"Dom","FLBR.HEADER.Modifiers":"Modyfikatory","FLBR.HEADER.PromotionPoints":"Punkty Promocji","FLBR.NATURE.Replicant":"Replikant","FLBR.ARCHETYPE.ANALYST":"Analityk","FLBR.ARCHETYPE.CITY_SPEAKER":"Miastowiec","FLBR.ARCHETYPE.DOXIE":"Lalka","FLBR.ARCHETYPE.ENFORCER":"Egzekutorka","FLBR.ARCHETYPE.FIXER":"Lawirantka","FLBR.ARCHETYPE.INSPECTOR":"Inspektor","FLBR.ARCHETYPE.SKIMMER":"Szubrawiec","FLBR.COMBAT_ACTION.Sprint":"Sprint","FLBR.COMBAT_ACTION.ShootFirearm":"Strzał z Broni Palnej","FLBR.COMBAT_ACTION_HINT.Sprint":"Możesz wykonać dodatkowe ruchy w jednej rundzie, korzystając z akcji sprintu. Ta akcja będzie jedyną możliwą do wykonania w danej rundzie. Rzućć na Mobilność - za każdy wyrzucony Sukces, możesz wykonać dodatkowy ruch oprócz darmowego. Jeśli nie uda ci się rzut, upadasz i nie możesz wstać do następnej tury.","FLBR.COMBAT_ACTION_HINT.Crawl":"Jeśli musisz się poruszać, będąc w pozycji leżącej, musisz pełzać. Działa to jak ruch, ale jest traktowane jako twoja akcja na tę rundę. Możesz wstać na końcu swojego pełzania (akcja darmowa).","FLBR.SKILL.Connections":"Kontakty","FLBR.SKILL.Insight":"Intuicja","FLBR.SKILL.Manipulation":"Perswazja","FLBR.SKILL.Driving":"Prowadzenie","FLBR.NATURE.Human":"Człowiek","FLBR.COMBAT_ACTION_HINT.BreakFree":"Siła + Walka Wręcz","FLBR.COMBAT_ACTION_HINT.ShootFirearm":"Zręczność + Strzelectwo","FLBR.COMBAT_ACTION_HINT.CarefulAim":"Jeśli poświęcisz czas na staranne celowanie przed naciśnięciem spustu, zyskujesz przewagę w rzucie ataku. Celowanie samo w sobie jest jednak akcją i nie możesz celować ostrożnie oraz strzelać w tej samej rundzie. Jeśli wykonasz jakąkolwiek inną akcję po celowaniu lub zostaniesz trafiony, tracisz efekt celowania i musisz poświęcić kolejną akcję, aby ponownie celować ostrożnie.","FLBR.COMBAT_ACTION_HINT.MeleeAttack":"Siła + Bijatyka","FLBR.COMBAT_ACTION_HINT.Grapple":"Jeśli złapiesz przeciwnika w walce wręcz jako specjalny atak, oboje upadacie na ziemię. Przeciwnik traci trzymaną broń i nie może się ruszać. Jedyną akcją, jaką może wykonać, jest próba wyzwolenia się, która odnosi sukces, jeśli przeciwnik wygra rzut na walkę wręcz przeciwko tobie. Podczas chwytania, jedyną twoją akcją (do momentu zwolnienia przeciwnika) jest atak chwytem. Działa to jak normalny atak bez broni, ale cel jest unieruchomiony i nie może się bronić.","FLBR.BUTTON.Delete":"Usuń przedmiot","FLBR.BUTTON.Edit":"Edytuj przedmiot","FLBR.BUTTON.Mount":"Zamontuj broń","FLBR.BUTTON.Roll":"Rzuć przedmiotem","FLBR.HEADER.DieSize":"Bazowa Kostka","FLBR.HEADER.FavoredGear":"Ulubiony Ekwipunek","FLBR.HEADER.Health":"Zdrowie","FLBR.HEADER.Hull":"Kadłub","FLBR.HEADER.HumanityPoints":"Punkty Humanitarności","FLBR.HEADER.Injuries":"Obrażenia","FLBR.BUTTON.ToChat":"Wyślij przedmiot do czatu","FLBR.BUTTON.Unmount":"Odłącz broń","FLBR.CHAT_ACTION.ApplyDamage":"Zastosuj obrażenia do wybranych celów","FLBR.DIALOG.AssignSuccessesHint":"Wybierz liczbę sukcesów do wykorzystania przeciwko {name}.","FLBR.DIALOG.ChooseAction":"Wybierz Akcję","FLBR.DIALOG.ChooseAttack":"Wybierz Atak","FLBR.HEADER.Inventory":"Ekwipunek","FLBR.HEADER.Notes":"Notatki","FLBR.HEADER.Actions":"Akcje","FLBR.HEADER.Resolve":"Opanowanie","FLBR.HEADER.SignatureItem":"Charakterystyczny Przedmiot","FLBR.HEADER.Specialties":"Specjalności","FLBR.HEADER.Appearance":"Wygląd","FLBR.HEADER.Armor":"Pancerz","FLBR.HEADER.Attacks":"Rodzaje Ataków","FLBR.HEADER.AttributesAndSkills":"Atrybuty i Umiejętności","FLBR.HEADER.Augments":"Ulepszenia","FLBR.HEADER.ChinyenPoints":"Punkty Chinyen","FLBR.HEADER.KeyMemory":"Kluczowe Wspomnienie","FLBR.HEADER.KeyRelationship":"Kluczowa Relacja","FLBR.HEADER.Rating":"Ocena","FLBR.HEADER.YearsOnTheForce":"Lata Służby","FLBR.HEADER.Weapons":"Bronie","FLBR.SHEET_CONFIG.CharacterSubtype":"Podtyp","FLBR.SHEET_CONFIG.HealthMaxModifier":"Modyfikator maksymalnego zdrowia","FLBR.SHEET_HEADER.ItemRoll":"Rzut Przedmiotem","FLBR.TAB.Actions":"Akcje","FLBR.TAB.Bio":"Biografia","FLBR.TAB.Combat":"Walka","FLBR.TAB.Info":"Informacja","FLBR.MACRO.GetActorTitle":"Wybierz Postać","FLBR.MACRO.RollAction":"Akcja {action}","FLBR.VEHICLE.Action.SpeedingHint":"Odpowiednik sprintu na piechotę, możesz użyć akcji przyspieszenia, aby wykonać dodatkowe ruchy w tej samej rundzie pojazdem.","FLBR.VEHICLE.Altitude":"Wysokość","FLBR.VEHICLE.ChoosePassenger":"Wybierz Pasażera","FLBR.VEHICLE.ChoosePassengerHint":"Wybierz jednego z pasażerów.","FLBR.VEHICLE.Components":"Komponenty","FLBR.VEHICLE.CrashDamageFlavor":"Obrażenia zadane załodze wskutek wypadku pojazdu","FLBR.VEHICLE.CreateActor":"Utworzyć postać?","FLBR.VEHICLE.CreateActorHint":"Czy chcesz zduplikować postać {name} przed dodaniem go do załogi?","FLBR.VEHICLE.Crew":"Załoga","FLBR.VEHICLE.DecreaseHull":"Zmniejsz maksymalną wartość Wytrzymałości kadłuba","FLBR.VEHICLE.DeleteSeat":"Zmniejsz liczbę pasażerów","FLBR.VEHICLE.EditPassenger":"Edytuj Pasażera","FLBR.VEHICLE.Engine":"Silnik","FLBR.VEHICLE.IncreaseHull":"Zwiększ maksymalną wartość Wytrzymałości kadłuba","FLBR.VEHICLE.MountedWeapons":"Zamontowane Bronie","FLBR.VEHICLE.NotifDropCrewLoot":"Umieść swoje śmieci w bagażniku!","FLBR.VEHICLE.NotifPassengerAlreadyPresent":"Pasażer już jest obecny!","FLBR.VEHICLE.RemovePassenger":"Usuń Pasażera","FLBR.WeaponDamage":"Uszkodzenia","FLBR.WeaponCrit":"Kość Krytyczna","FLBR.WEAPON_RANGE.Long":"Długi","FLBR.WEAPON_RANGE.Extreme":"Ekstremalny","FLBR.Wrecked":"Zniszczony","FLBR.YouAreBroken":"Jesteś Pokonany!","YZEC.CombatTracker.FastAction":"Ruch","YZUR.CHAT.ROLL.Infos":"Informacje","YZUR.CHAT.ROLL.Damage":"Podstawowe Obrażenia","YZUR.CHAT.ROLL.Push":"Przerzut","YZUR.CHAT.ROLL.CancelPush":"Akceptuj","YZUR.DIETERMS.D6BladeRunnerDie":"K6","YZUR.DIETERMS.D8BladeRunnerDie":"K8","YZUR.DIETERMS.D10BladeRunnerDie":"K10","YZUR.DIETERMS.D12BladeRunnerDie":"K12","TYPES.Actor.character":"Postać","TYPES.Item.generic":"Ogólny","SETTINGS.BLADE_RUNNER.EditNaturePermissionName":"Pozwól na Zmianę Typu","SETTINGS.BLADE_RUNNER.EditNaturePermissionHint":"Czy pozwolić graczowi na edycję jego typu na karcie postaci.","SETTINGS.BLADE_RUNNER.OpenFirstWeaponAttackName":"Rozpocznij Pierwszy Atak Bronią","SETTINGS.BLADE_RUNNER.OpenFirstWeaponAttackHint":"Czy otworzyć zwijaną sekcję pierwszego ataku bronią po wyświetleniu karty przedmiotu.","SETTINGS.BLADE_RUNNER.UpdateActorManeuverabilityOnCrewName":"Aktualizuj Manewrowość Przy Dodaniu Do Załogi","SETTINGS.BLADE_RUNNER.UpdateActorManeuverabilityOnCrewHint":"Czy aktualizować manewrowość postacji przy dodaniu go do pojazdu.","SETTINGS.BLADE_RUNNER.UpdateActorManeuverabilityOnUncrewName":"Aktualizuj Manewrowość Przy Usunięciu Z Załogi","FLBR.SHEET_CONFIG.CharacterSubtypeHint":"Wybierz, czy to jest PC (Postać Gracza) czy NPC (Postać Niezależna). Druga opcja zapewnia uproszczoną kartę postaci oraz dodatkowe funkcje dla Mistrza Gry, ułatwiające prowadzenie NPC.","FLBR.SHEET_CONFIG.HealthMaxModifierHint":"Modyfikuj maksymalną pojemność zdrowia (Tylko dla MG).","FLBR.SHEET_CONFIG.HealthPermanentLoss":"Stała utrata zdrowia","FLBR.SHEET_CONFIG.HealthPermanentLossHint":"Ustaw, ile zdrowia jest trwale utracone (Tylko dla MG).","FLBR.SHEET_CONFIG.ResolveMaxModifier":"Modyfikator maksymalnego opanowania","FLBR.SHEET_CONFIG.ResolveMaxModifierHint":"Modyfikuj maksymalną pojemność opanowania (Tylko dla MG).","FLBR.SHEET_CONFIG.ResolvePermanentLoss":"Stała utrata opanowania","FLBR.SHEET_CONFIG.ResolvePermanentLossHint":"Ustaw, ile opanowania jest trwale utracone (Tylko dla MG).","FLBR.SHEET_CONFIG.Maneuverability":"Manewrowość pojazdu","FLBR.SHEET_CONFIG.ManeuverabilityHint":"Ustaw manewrowość prowadzonego pojazdu, jeśli ma to zastosowanie.","FLBR.SHEET_CONFIG.UnlimitedPush":"Pozwól na nieograniczone przerzuty","FLBR.SHEET_CONFIG.UnlimitedPushHint":"Jeśli twój MG pozwala na nieograniczoną liczbę przerzutów. Sprawdź to ustawienie, aby wyświetlić przycisk przerzutu przy prawie każdym rzucie.","FLBR.SHEET_HEADER.ArmorRoll":"Rzut na Pancerz","FLBR.SHEET_HEADER.BlastPowerRoll":"Rzut na Moc Wybuchu","FLBR.SHEET_HEADER.GenericRoll":"Ogólny Rzut","FLBR.SHEET_HEADER.ItemPost":"Opublikuj Przedmiot","FLBR.TAB.Inventory":"Ekwipunek","FLBR.TAB.Features":"Cechy","FLBR.TAB.Mods":"Modyfikacje","FLBR.TAB.Stats":"Statystyki","FLBR.D":"K","FLBR.ACTIVE_EFFECT.Add":"Dodaj Aktywny Efekt","FLBR.ACTIVE_EFFECT.Delete":"Usuń Efekt","FLBR.ACTIVE_EFFECT.Edit":"Edytuj Efekt","FLBR.ACTIVE_EFFECT.Empty":"Brak Efektu.","FLBR.ACTIVE_EFFECT.New":"Nowy Efekt","FLBR.ACTIVE_EFFECT.Toggle":"Przełącz Efekt","FLBR.ACTIVE_EFFECT.InActorDisclaimer":"Uwaga: Aktywne efekty nie mogą być edytowane w osadzonych przedmiotach (ograniczenie Foundry).","FLBR.Advantage":"Ułatwienie","FLBR.Capacities":"Zdrowie & Opanowanie","FLBR.Blast":"Wybuch","FLBR.BlastPower":"Moc Wybuchu","FLBR.Broken":"Zepsuty","FLBR.BrokenByDamage":"Zepsuty przez Obrażenia","FLBR.Cancel":"Anuluj","FLBR.BrokenByStress":"Zepsuty przez Stres","FLBR.CHAT.DamageDealt":"{name} otrzymuje {amount} obrażeń.","FLBR.CHAT.NoDamageDealt":"{name} nie otrzymał obrażeń.","FLBR.CHAT.InitialDamageDealt":"Atak spowodował początkowo {damage} obrażeń.","FLBR.CHAT.CritInflicted":"{name} doznał krytycznego obrażenia {crit}.","FLBR.COMBAT.Combat":"Walka","FLBR.COMBAT.CannotBeDamaged":"Nie można zadać obrażeń","FLBR.CHAT.ArmorDeflectedDamage":"Pancerz odparł {damage} obrażeń.","FLBR.CHAT.ArmorDeflectedAllDamage":"Pancerz odparł wszystkie obrażenia.","FLBR.CHAT.ArmorNoEffect":"Pancerz nie ma efektu.","FLBR.COMBAT.DrawSize":"Rozmiar kostki","FLBR.COMBAT.SkippedDefender":"Pominięto obrońcę {name}","FLBR.CreateNewItem":"Stwórz nowy przedmiot {type}","FLBR.CRIT.Crit":"Krytyk","FLBR.CRIT.CriticalInjury":"Krytyczne Obrażenie","FLBR.CRIT.ChooseCrit":"Wybierz Krytyczne Obrażenie","FLBR.CRIT.ChooseVehicleDamage":"Wybierz Krytyczne Uszkodzenie Pojazdu","FLBR.CRIT.ChooseActor":"Wybierz postać, która ma otrzymać krytyczne obrażenie.","FLBR.CRIT.DrawCrit":"Wylosuj Krytyczne Obrażenie","FLBR.CRIT.DrawVehicleDamage":"Wylosuj Krytyczne Uszkodzenie Pojazdu","FLBR.CustomFormula":"Własna Formuła","FLBR.Disadvantage":"Utrudnienie","FLBR.Formula":"Formuła","FLBR.CRIT.CritButtonTooltip":"Wylosuj krytyczne obrażenie dla postaci gracza lub wybranego znacznika","FLBR.InjuryCategory":"Kategoria Obrażeń","FLBR.InjuryDeathSave":"Rzut na Śmierć","FLBR.InjuryHealing":"Leczenie","FLBR.InjuryLethal":"Śmiertelne","FLBR.InjuryPermanent":"Trwałe","FLBR.InventoryNoItem":"Pusty.","FLBR.ItemAddAction":"Dodaj Akcję","FLBR.ItemAddAttack":"Dodaj Atak","FLBR.ItemAddModifier":"Dodaj Modyfikator Rzutu","FLBR.ItemActionAttribute":"Atrybut","FLBR.ItemActionName":"Nazwa Akcji","FLBR.ItemActionSkill":"Umiejętność","FLBR.ItemActionType":"Typ Akcji","FLBR.ItemActionTypeRollSkill":"Rzut Umiejętnością","FLBR.ItemActionTypeRunMacro":"Uruchom Makro","FLBR.ItemAttackName":"Nazwa Ataku","FLBR.ItemArmor":"Pancerz","FLBR.ITEM_AVAILABILITY.Incidental":"Przypadkowy","FLBR.ITEM_AVAILABILITY.Luxury":"Luksusowy","FLBR.ITEM_AVAILABILITY.Premium":"Premium","FLBR.ITEM_AVAILABILITY.Rare":"Rzadki","FLBR.ITEM_AVAILABILITY.Standard":"Standardowy","FLBR.ItemAvailability":"Dostępność","FLBR.ItemConsumable":"Zużywalny","FLBR.ItemConsumeNotif":"Zużyto {qty} jednostek {name}","FLBR.ItemCost":"Koszt","FLBR.ItemDeleteAction":"Usuń Akcję","FLBR.ItemDeleteAttack":"Usuń Atak","FLBR.ItemDeleteModifier":"Usuń Modyfikator","FLBR.ItemNewAction":"Nowa Akcja","FLBR.ItemNewAttack":"Nowy Atak","FLBR.ItemNoAttack":"Brak zdefiniowanego trybu ataku.","FLBR.ItemNoModifier":"Brak modyfikatora.","FLBR.ItemModifier":"Modyfikator","FLBR.ItemMounted":"Zamontowany","FLBR.ItemQuantity":"Ilość","FLBR.ItemSpecial":"Specjalny","FLBR.MACRO.GetActorHint":"Wybrałeś wiele znaczników. Proszę wybierz jednego z nich.","FLBR.MACRO.MultipleItems":"Twoja postać {actor} posiada więcej niż jeden przedmiot o nazwie {item}. Wybrany zostanie pierwszy pasujący przedmiot.","FLBR.MACRO.NoActor":"Twoja postać {actor} nie została znaleziona.","FLBR.NatureHint":"Typ postaci (Tylko dla MG)","FLBR.NewItem":"Nowy {type}","FLBR.OK":"OK","FLBR.Range":"Zasięg","FLBR.RANGED_COMBAT.OutOfRange":"Poza zasięgiem","FLBR.RANGED_COMBAT.SmallTarget":"Mały cel","FLBR.RANGED_COMBAT.LargeTarget":"Duży cel","FLBR.RANGED_COMBAT.TargetBehindCover":"Cel za osłoną","FLBR.RANGED_COMBAT.CarefulAim":"Precyzyjne celowanie","FLBR.RANGED_COMBAT.UnseenTarget":"Cel niewidoczny","FLBR.ROLLER.AddDie":"Dodaj kość","FLBR.MACRO.NoItem":"Twoja postać {actor} nie posiada przedmiotu o nazwie {item}.","FLBR.MACRO.RollStat":"Rzut {stat}","FLBR.Nature":"Typ","FLBR.ROLLER.MaxPush":"Maksymalny przerzut","FLBR.ROLLER.ModifierActive":"Aktywny","FLBR.ROLLER.ModifierName":"Nazwa","FLBR.ROLLER.ModifierValue":"Wartość","FLBR.ROLLER.TotalModifier":"Suma: {value}","FLBR.ROLLER.ResolveTest":"Test na stałą utratę Opanowania","FLBR.ROLLER.ResolveTestHint":"Kiedy jesteś złamany przez stres, musisz rzucić jedną kością bazową odpowiadającą twojej Empatii. Jeśli wyrzucisz jedynkę, tracisz na stałe 1 punkt Opanowania, co oznacza redukcję maksymalnego Opanowania o jeden stopień.","FLBR.ROLLER.ResolveTestFailed":"{name} nie zdał testu na Empatię i traci na stałe 1 punkt opanowania.","FLBR.ROLLER.RollButton":"Rzuć","FLBR.ROLLER.RollSpecialHint":"Rzut: {name}?","FLBR.ROLLER.RollWithAdvantageButton":"Ułatwienie","FLBR.ROLLER.RollWithDisadvantageButton":"Utrudnienie","FLBR.ROLLER.SelectDiceToPush":"Wybierz kości do przerzutu","FLBR.ROLLER.UnlimitedPush":"Nieograniczony przerzut","FLBR.SignatureItemUsedCheckboxHint":"Użyte?","FLBR.SomeoneIsSomething":"{name} jest {status}","FLBR.VEHICLE.Action.Crash":"Wypadek","FLBR.VEHICLE.Action.CrashHint":"Jeśli Spinner jest rozbity, uderza gwałtownie w ziemię. Wszyscy znajdujący się w środku doznają obrażeń upadkowych K3 punktów plus jeden za każdą strefę wysokości, zredukowane o wartość pancerza pojazdu. Możesz również przygotować się na uderzenie, rzucając na ZWINNOŚĆ - każde wyrzucone S zmniejsza obrażenia o jeden.","FLBR.VEHICLE.Action.Repair":"Naprawa","FLBR.VEHICLE.Action.CrashDialog":"To spowoduje rozbicie pojazdu i zada {damage} obrażeń upadkowych każdemu pasażerowi, zmniejszone o wartość pancerza pojazdu i rzut na ZWINNOŚĆ (bez akcji).","FLBR.VEHICLE.Action.Explode":"Eksplozja","FLBR.VEHICLE.Action.ExplodeHint":"Pojazd eksploduje w kuli ognia. Wszyscy znajdujący się w środku są narażeni na wybuch o mocy B, a pojazd zostaje zniszczony poza możliwością naprawy.","FLBR.VEHICLE.Action.MassiveCrash":"Ogromny Wypadek","FLBR.VEHICLE.Action.Ramming":"Taranowanie","FLBR.VEHICLE.Action.RammingHint":"Większość pojazdów może być używana jako broń - np. do przejechania celu. Liczy się to jako atak wręcz, ale zamiast walki wręcz wykonuje się rzut na PROWADZENIE. Ten rzut może być połączony z akcją przyspieszenia, ale wtedy rzut na PROWADZENIE staje się rzutem przeciwnystawnym, co może zmniejszyć skuteczność przyspieszenia. Podstawowy poziom obrażeń ataku równa się połowie wartości Wytrzymałości kadłuba pojazdu, zaokrąglając w górę. Taranowanie pojazdów powoduje obrażenia miażdżące, a kość krytyczna ma wielkość równą Wytrzymałości kadłuba, zaokrągloną w górę do najbliższej kości, do maksymalnej wartości K12 - np. pojazd o kadłubie 9 ma kość krytyczną K10.","FLBR.VEHICLE.Action.RepairHint":"Naprawa uszkodzeń pojazdu wymaga jednego lub kilku rzutów na Technika. Jeden rzut można wykonać na zmianę. Tylko jedna osoba może rzucać na naprawę kadłuba, ale inni mogą pomagać. Każde wyrzucone S usuwa jeden punkt uszkodzeń. Jeśli pojazd był zniszczony, przestaje być uznawany za zniszczony po naprawie jednego punktu uszkodzeń. Jeśli silnik lub zamontowana broń zostały unieruchomione wskutek krytycznego trafienia, naprawa zazwyczaj wymaga rzutu na Technika, oprócz jakiegokolwiek rzutu na naprawę uszkodzeń kadłuba. Praca trwa jedną zmianę.","FLBR.VEHICLE.Action.Speeding":"Przyspieszanie","FLBR.VEHICLE.AddSeat":"Zwiększ liczbę pasażerów (tylko dla MG)","FLBR.VEHICLE.NotifCrewFull":"Brak wolnych miejsc!","FLBR.VEHICLE.NotifDropCrewVehicle":"Wkładanie pojazdów do pojazdów jest niedozwolone!","FLBR.WeaponDamageType":"Rodzaj Obrażeń","FLBR.WEAPON_DAMAGE_TYPE.Crushing":"Miażdżące","FLBR.WEAPON_DAMAGE_TYPE.Piercing":"Przebijające","FLBR.WEAPON_DAMAGE_TYPE.Stress":"Stres","FLBR.WeaponRange":"Zasięg","FLBR.WEAPON_RANGE.Engaged":"Zaangażowany","FLBR.WEAPON_RANGE.Short":"Krótki","FLBR.WEAPON_RANGE.Medium":"Średni","FLBR.ActorSheet.NotifWrongItemType":"Aktor {actor} nie może posiadać przedmiotów typu {type}!","YZEC.CombatTracker.SlowAction":"Akcja","TYPES.Actor.vehicle":"Pojazd","TYPES.Actor.loot":"Łup","ACTOR.SubtypePc":"Postać Gracza","ACTOR.SubtypeNpc":"Postać Niezależna","TYPES.Item.upgrade":"Ulepszenie","TYPES.Item.armor":"Pancerz","TYPES.Item.weapon":"Broń","TYPES.Item.explosive":"Materiał Wybuchowy","TYPES.Item.specialty":"Specjalność","TYPES.Item.injury":"Obrażenie","GAME.Paused":"Śledztwo Wstrzymane","SETTINGS.BLADE_RUNNER.AutomaticApplyDamageName":"Automatyczne Zastosowanie Obrażeń","SETTINGS.BLADE_RUNNER.AutomaticApplyDamageHint":"Czy automatycznie stosować obrażenia z rzutów do wybranych tokenów i pasażerów pojazdów.","SETTINGS.BLADE_RUNNER.AutomaticArmorRollName":"Automatyczny Rzut na Pancerz","SETTINGS.BLADE_RUNNER.AutomaticArmorRollHint":"Czy automatycznie rzucać na pancerz.","SETTINGS.BLADE_RUNNER.CrashTableName":"Tabela Uszkodzeń Pojazdu","SETTINGS.BLADE_RUNNER.CrashTableHint":"ID lub nazwa tabeli krytycznych uszkodzeń pojazdów.","SETTINGS.BLADE_RUNNER.CrushingTableName":"Tabela Obrażeń Miażdżących","SETTINGS.BLADE_RUNNER.CrushingTableHint":"ID lub nazwa tabeli krytycznych obrażeń miażdżących.","SETTINGS.BLADE_RUNNER.PiercingTableName":"Tabela Obrażeń Przebijających","SETTINGS.BLADE_RUNNER.PiercingTableHint":"ID lub nazwa tabeli krytycznych obrażeń przebijających.","SETTINGS.BLADE_RUNNER.UpdateActorManeuverabilityOnUncrewHint":"Czy aktualizować manewrowość postacji przy usunięciu go z pojazdu.","SETTINGS.BLADE_RUNNER.UseActiveEffectsName":"Używaj Aktywnych Efektów","SETTINGS.BLADE_RUNNER.UseActiveEffectsHint":"Czy wyświetlać dodatkowe pole w kartach postaci do zarządzania Aktywnymi Efektami (dla zaawansowanych graczy).","WARNING.ApplyDamageNoNature":"Zastosuj Obrażenia Pchnięcia: Nieznany Typ Postaci!","WARNING.ApplyDamageNoCapacityKey":"Zastosuj Obrażenia Pchnięcia: Nie Znaleziono Relacji Typ-Wytrzymałość!","WARNING.ApplyDamageNoActorCapacity":"Zastosuj Obrażenia Pchnięcia: Wytrzymałość Postacji Nieznana!","WARNING.NoDiceInput":"Brak kości do rzutu!","WARNING.NoItemAction":"Brak zdefiniowanej akcji przedmiotu!","WARNING.ItemActionMacroNotFound":"Makro \"{macro}\" nie znalezione!","WARNING.NoItemUnit":"Nie można użyć {name}: brak jednostek!","SETTINGS.BLADE_RUNNER.DoNotUseHandwrittenFontName":"Nie używaj pisma odręcznego","SETTINGS.BLADE_RUNNER.DoNotUseHandwrittenFontHint":"Wyłącz użycie odręcznego fontu \"Caveat\" w kartach postaci."} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..1fd3838 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,189 @@ +{ + "name": "slazero2", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "classic-level": "^3.0.0" + } + }, + "node_modules/abstract-level": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/abstract-level/-/abstract-level-3.1.1.tgz", + "integrity": "sha512-CW2gKbJFTuX1feMvOrvsVMmijAOgI9kg2Ie9Dq3gOcMt/dVVoVmqNlLcEUCT13NxHFMEajcUcVBIplbyDroDiw==", + "license": "MIT", + "dependencies": { + "buffer": "^6.0.3", + "is-buffer": "^2.0.5", + "level-supports": "^6.2.0", + "level-transcoder": "^1.0.1", + "maybe-combine-errors": "^1.0.0", + "module-error": "^1.0.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/classic-level": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/classic-level/-/classic-level-3.0.0.tgz", + "integrity": "sha512-yGy8j8LjPbN0Bh3+ygmyYvrmskVita92pD/zCoalfcC9XxZj6iDtZTAnz+ot7GG8p9KLTG+MZ84tSA4AhkgVZQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "abstract-level": "^3.1.0", + "module-error": "^1.0.1", + "napi-macros": "^2.2.2", + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/level-supports": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/level-supports/-/level-supports-6.2.0.tgz", + "integrity": "sha512-QNxVXP0IRnBmMsJIh+sb2kwNCYcKciQZJEt+L1hPCHrKNELllXhvrlClVHXBYZVT+a7aTSM6StgNXdAldoab3w==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/level-transcoder": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/level-transcoder/-/level-transcoder-1.0.1.tgz", + "integrity": "sha512-t7bFwFtsQeD8cl8NIoQ2iwxA0CL/9IFw7/9gAjOonH0PWTTiRfY7Hq+Ejbsxh86tXobDQ6IOiddjNYIfOBs06w==", + "license": "MIT", + "dependencies": { + "buffer": "^6.0.3", + "module-error": "^1.0.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/maybe-combine-errors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/maybe-combine-errors/-/maybe-combine-errors-1.0.0.tgz", + "integrity": "sha512-eefp6IduNPT6fVdwPp+1NgD0PML1NU5P6j1Mj5nz1nidX8/sWY7119WL8vTAHgqfsY74TzW0w1XPgdYEKkGZ5A==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/module-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/module-error/-/module-error-1.0.2.tgz", + "integrity": "sha512-0yuvsqSCv8LbaOKhnsQ/T5JhyFlCYLPXK3U2sgV10zoKQwzs/MyfuQUOZQ1V/6OCOJsK/TRgNVrPuPDqtdMFtA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/napi-macros": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/napi-macros/-/napi-macros-2.2.2.tgz", + "integrity": "sha512-hmEVtAGYzVQpCKdbQea4skABsdXW4RUh5t5mJ2zzqowJS2OyXZTU1KhDVFhx+NlWZ4ap9mqR9TcDO3LTTttd+g==", + "license": "MIT" + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..55cf153 --- /dev/null +++ b/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "classic-level": "^3.0.0" + } +} diff --git a/packs/ammo.json b/packs/ammo.json new file mode 100644 index 0000000..911f0a5 --- /dev/null +++ b/packs/ammo.json @@ -0,0 +1,210 @@ +[ + { + "name": "10mm Standard Rounds (x30)", + "type": "ammo", + "img": "systems/slazero2/assets/SLA_Assets/Ammo/10mm-pistol__STD.png", + "system": { + "description": "Standard 10mm full metal jacket rounds. The most common pistol and SMG calibre on Mort. Available at every Shiver station, most corner shops, and all operative supply depots.", + "caliber": "10mm", + "roundType": "standard", + "quantity": 30, + "costPerRound": 2, + "apBonus": 0, + "damageMod": 0, + "blastRadius": 0, + "weight": 1 + } + }, + { + "name": "10mm AP Rounds (x30)", + "type": "ammo", + "img": "systems/slazero2/assets/SLA_Assets/Ammo/10mm-pistol__AP.png", + "system": { + "description": "Armour-piercing 10mm rounds with a hardened steel penetrator core. Each hit reduces target's armour dice by 1 (in addition to weapon AP). More expensive but essential against armoured targets.", + "caliber": "10mm", + "roundType": "AP", + "quantity": 30, + "costPerRound": 8, + "apBonus": 1, + "damageMod": 0, + "blastRadius": 0, + "weight": 1 + } + }, + { + "name": "9mm Standard Rounds (x30)", + "type": "ammo", + "img": "systems/slazero2/assets/SLA_Assets/Ammo/10mm-smg__STD.png", + "system": { + "description": "Standard 9mm parabellum. Compatible with the FEN 603 and similar machine pistols. High velocity, moderate stopping power.", + "caliber": "9mm", + "roundType": "standard", + "quantity": 30, + "costPerRound": 2, + "apBonus": 0, + "damageMod": 0, + "blastRadius": 0, + "weight": 1 + } + }, + { + "name": "5.56mm Standard Rounds (x30)", + "type": "ammo", + "img": "systems/slazero2/assets/SLA_Assets/Ammo/5mm-rifle__STD.png", + "system": { + "description": "Standard 5.56mm intermediate rifle rounds. Fits the FEN AR and compatible assault rifles. High velocity, good accuracy at medium range.", + "caliber": "5.56mm", + "roundType": "standard", + "quantity": 30, + "costPerRound": 3, + "apBonus": 0, + "damageMod": 0, + "blastRadius": 0, + "weight": 1 + } + }, + { + "name": "5.56mm AP Rounds (x30)", + "type": "ammo", + "img": "systems/slazero2/assets/SLA_Assets/Ammo/5mm-rifle__AP.png", + "system": { + "description": "5.56mm armour-piercing rounds for assault rifles. Penetrator core defeats light armour. Essential for engagements against armoured Shivers or operative-grade targets.", + "caliber": "5.56mm", + "roundType": "AP", + "quantity": 30, + "costPerRound": 10, + "apBonus": 1, + "damageMod": 0, + "blastRadius": 0, + "weight": 1 + } + }, + { + "name": "7.62mm Standard Rounds (x20)", + "type": "ammo", + "img": "systems/slazero2/assets/SLA_Assets/Ammo/7mm-rifle__STD.png", + "system": { + "description": "Full-power 7.62mm rifle rounds. Fits the Browbeater series, FEN Reaper LMG, and similar battle rifles. More penetrating than intermediate calibres.", + "caliber": "7.62mm", + "roundType": "standard", + "quantity": 20, + "costPerRound": 4, + "apBonus": 0, + "damageMod": 0, + "blastRadius": 0, + "weight": 1 + } + }, + { + "name": "7.62mm AP Rounds (x20)", + "type": "ammo", + "img": "systems/slazero2/assets/SLA_Assets/Ammo/7mm-rifle__AP.png", + "system": { + "description": "7.62mm armour-piercing for battle rifles and LMGs. Defeats medium armour consistently. Standard upgrade for sustained-fire engagements against armoured targets.", + "caliber": "7.62mm", + "roundType": "AP", + "quantity": 20, + "costPerRound": 12, + "apBonus": 2, + "damageMod": 0, + "blastRadius": 0, + "weight": 1 + } + }, + { + "name": "12-Gauge Standard Shells (x8)", + "type": "ammo", + "img": "systems/slazero2/assets/SLA_Assets/Ammo/12-gauge__STD.png", + "system": { + "description": "Standard 12-gauge buckshot shells. Devastating at close range, poor at anything else. Each shell fires a spread pattern covering Engaged range with full damage, Short range at -1 damage.", + "caliber": "12g", + "roundType": "standard", + "quantity": 8, + "costPerRound": 5, + "apBonus": 0, + "damageMod": 0, + "blastRadius": 0, + "weight": 1 + } + }, + { + "name": "12-Gauge Slug Rounds (x8)", + "type": "ammo", + "img": "systems/slazero2/assets/SLA_Assets/Ammo/12-gauge__AP.png", + "system": { + "description": "12-gauge rifled slugs. A single heavy projectile instead of a spread pattern. Retains full damage at Short range. Gains AP 1. Less area coverage but better penetration than buckshot.", + "caliber": "12g", + "roundType": "slug", + "quantity": 8, + "costPerRound": 8, + "apBonus": 1, + "damageMod": 0, + "blastRadius": 0, + "weight": 1 + } + }, + { + "name": ".44 Standard Rounds (x6)", + "type": "ammo", + "img": "systems/slazero2/assets/SLA_Assets/Ammo/10mm-pistol__STD.png", + "system": { + "description": "Heavy .44 revolver rounds for the BLA 046M Blitzer. Slow to reload, high stopping power. Each round hits harder than comparable pistol calibres.", + "caliber": ".44", + "roundType": "standard", + "quantity": 6, + "costPerRound": 6, + "apBonus": 0, + "damageMod": 0, + "blastRadius": 0, + "weight": 0 + } + }, + { + "name": ".454 Standard Rounds (x12)", + "type": "ammo", + "img": "systems/slazero2/assets/SLA_Assets/Ammo/12mm__STD.png", + "system": { + "description": "Heavy-calibre pistol rounds for the FEN 204 Heavy Pistol. High recoil, high damage. The round that makes the compensator necessary.", + "caliber": ".454", + "roundType": "standard", + "quantity": 12, + "costPerRound": 7, + "apBonus": 0, + "damageMod": 0, + "blastRadius": 0, + "weight": 0 + } + }, + { + "name": "40mm HE Grenade Round (x1)", + "type": "ammo", + "img": "systems/slazero2/assets/SLA_Assets/Ammo/40mm-he-grenade__HE.png", + "system": { + "description": "High-explosive 40mm grenade round for the MILA launcher. On impact, deals DMG 5 within Engaged range of blast point, DMG 2 at Short range. Armour dice halved against blast.", + "caliber": "40mm", + "roundType": "HE", + "quantity": 1, + "costPerRound": 80, + "apBonus": 0, + "damageMod": 2, + "blastRadius": 3, + "weight": 1 + } + }, + { + "name": "40mm Smoke Round (x1)", + "type": "ammo", + "img": "systems/slazero2/assets/SLA_Assets/Ammo/40mm-smoke-gas__STD.png", + "system": { + "description": "40mm smoke grenade round for the MILA launcher. Creates a dense smoke cloud at Short range radius on impact. All ranged attacks through the cloud are at -2 dice. Dissipates after 3 rounds.", + "caliber": "40mm", + "roundType": "smoke", + "quantity": 1, + "costPerRound": 40, + "apBonus": 0, + "damageMod": 0, + "blastRadius": 3, + "weight": 1 + } + } +] \ No newline at end of file diff --git a/packs/armors.json b/packs/armors.json new file mode 100644 index 0000000..8d7e5a4 --- /dev/null +++ b/packs/armors.json @@ -0,0 +1,308 @@ +[ + { + "name": "Body Blocker", + "type": "armor", + "img": "systems/slazero2/assets/icons/armor/armor-body-blocker.svg", + "system": { + "description": "A slim concealable ballistic vest designed to be worn under civilian clothing. Popular with corporate workers, contract personnel, and anyone who wants protection without advertising it. Lightweight and unobtrusive \u2014 you barely notice it until a round hits.", + "armorRating": 1, + "armorDice": 1, + "armorAuto": 0, + "statMod": 0, + "skillMod": 0, + "statModTarget": "", + "skillModTarget": "", + "cost": 120, + "initiativeMod": 0 + } + }, + { + "name": "CAF Armor", + "type": "armor", + "img": "systems/slazero2/assets/SLA_Assets/Armor/CAF Armour.png", + "system": { + "description": "Standard CAF tactical vest and pads; decent protection without much extra bulk.", + "armorRating": 2, + "armorDice": 2, + "armorAuto": 1, + "statMod": 0, + "skillMod": 0, + "statModTarget": "", + "skillModTarget": "", + "cost": 400, + "initiativeMod": 0 + } + }, + { + "name": "Crackshot Armour", + "type": "armor", + "img": "systems/slazero2/assets/SLA_Assets/Armor/2nd try/Crackshot.png", + "system": { + "description": "Lightweight precision-operative armour designed for marksmanship. Minimal shoulder and torso coverage to avoid restricting aiming motion. Includes integrated rangefinder mounting point.", + "armorRating": 2, + "armorDice": 2, + "armorAuto": 0, + "statMod": 1, + "skillMod": 0, + "statModTarget": "agility", + "skillModTarget": "", + "cost": 600, + "initiativeMod": 0 + } + }, + { + "name": "DN Power Armour", + "type": "armor", + "img": "systems/slazero2/assets/SLA_Assets/Armor/2nd try/Dark Night Power.png", + "system": { + "description": "Department of Nanotech powered combat armour. Lighter than standard power armour but with advanced nano-repair capability \u2014 the suit recovers 1 armour die between scenes. Rare and expensive.", + "armorRating": 5, + "armorDice": 5, + "armorAuto": 2, + "statMod": 1, + "skillMod": -1, + "statModTarget": "strength", + "skillModTarget": "stealth", + "cost": 7500, + "initiativeMod": 0 + } + }, + { + "name": "Downtown Jacket", + "type": "armor", + "img": "systems/slazero2/assets/SLA_Assets/Armor/Downtown Jacket.png", + "system": { + "description": "Reinforced street jacket with hidden ballistic panels; common among low-level gangers and worried civilians.", + "armorRating": 1, + "armorDice": 1, + "armorAuto": 0, + "statMod": 0, + "skillMod": 0, + "statModTarget": "", + "skillModTarget": "", + "cost": 150, + "initiativeMod": 0 + } + }, + { + "name": "Ebb Deathsuit", + "type": "armor", + "img": "systems/slazero2/assets/SLA_Assets/Armor/2nd try/Ebb Dethsuit.png", + "system": { + "description": "A ritualistically modified suit of armour channelled with sustained Ebb energy. Worn exclusively by elite Ebon operatives. Provides significant protection and adds +1 die to all Ebb rolls. Non-Ebb users cannot attune it.", + "armorRating": 4, + "armorDice": 4, + "armorAuto": 1, + "statMod": 0, + "skillMod": 1, + "statModTarget": "", + "skillModTarget": "ebb", + "cost": 5000, + "initiativeMod": 0 + } + }, + { + "name": "Exo-Rig Armor", + "type": "armor", + "img": "systems/slazero2/assets/SLA_Assets/Armor/Exo Armor.png", + "system": { + "description": "Powered exoskeletal frame with armored plates; massively increases carrying and weapon handling at the cost of finesse.", + "armorRating": 5, + "armorDice": 5, + "armorAuto": 2, + "statMod": 1, + "skillMod": -1, + "statModTarget": "strength", + "skillModTarget": "mobility", + "cost": 3000, + "initiativeMod": 0 + } + }, + { + "name": "Full Shiver Armour", + "type": "armor", + "img": "systems/slazero2/assets/SLA_Assets/Armor/2nd try/Shiver.png", + "system": { + "description": "Full plate and vest armour used by Shiver combat units. Rated for street operations and riot suppression. Includes shoulder guards, arm vambraces, and shin protection.", + "armorRating": 2, + "armorDice": 2, + "armorAuto": 0, + "statMod": 0, + "skillMod": 0, + "statModTarget": "", + "skillModTarget": "", + "cost": 450, + "initiativeMod": 0 + } + }, + { + "name": "HARD Armor", + "type": "armor", + "img": "systems/slazero2/assets/SLA_Assets/Armor/Hard Armor.png", + "system": { + "description": "Full-body ceramic and metal laminate suit for frontline combat and urban assaults.", + "armorRating": 4, + "armorDice": 4, + "armorAuto": 2, + "statMod": -1, + "skillMod": 1, + "statModTarget": "agility", + "skillModTarget": "force", + "cost": 1200, + "initiativeMod": 0 + } + }, + { + "name": "Heavy Thresher Suit", + "type": "armor", + "img": "systems/slazero2/assets/SLA_Assets/Armor/2nd try/Heavy Thresher.png", + "system": { + "description": "Full-coverage industrial combat suit with reinforced joints and integrated sealing system. Capable of operating in vacuum or toxic environments. Movement is restricted but survivability is exceptional.", + "armorRating": 5, + "armorDice": 5, + "armorAuto": 3, + "statMod": -1, + "skillMod": -2, + "statModTarget": "agility", + "skillModTarget": "mobility", + "cost": 6000, + "initiativeMod": -1 + } + }, + { + "name": "Light Thresher Suit", + "type": "armor", + "img": "systems/slazero2/assets/SLA_Assets/Armor/2nd try/Light Thresher.png", + "system": { + "description": "An industrial hardsuit adapted for combat use. Angular composite plates with mounting points for additional equipment. Designed for hostile environment operations as much as firefights.", + "armorRating": 4, + "armorDice": 4, + "armorAuto": 2, + "statMod": 0, + "skillMod": -1, + "statModTarget": "", + "skillModTarget": "stealth", + "cost": 3500, + "initiativeMod": 0 + } + }, + { + "name": "PP10 HARD Armour", + "type": "armor", + "img": "systems/slazero2/assets/SLA_Assets/Armor/Hard Armor.png", + "system": { + "description": "An uprated HARD Armour system with PP10 composite plating. Heavier than standard HARD but capable of stopping rounds that would punch through lighter suits.", + "armorRating": 5, + "armorDice": 4, + "armorAuto": 2, + "statMod": -1, + "skillMod": 0, + "statModTarget": "agility", + "skillModTarget": "", + "cost": 2800, + "initiativeMod": -1 + } + }, + { + "name": "Scout Armor", + "type": "armor", + "img": "systems/slazero2/assets/SLA_Assets/Armor/Scout Armor.png", + "system": { + "description": "Lightweight composite plates over flexible mesh; built for operatives who need to move and climb.", + "armorRating": 3, + "armorDice": 3, + "armorAuto": 1, + "statMod": 1, + "skillMod": 0, + "statModTarget": "agility", + "skillModTarget": "", + "cost": 700, + "initiativeMod": 0 + } + }, + { + "name": "Shiver Flak Vest", + "type": "armor", + "img": "systems/slazero2/assets/SLA_Assets/Armor/2nd try/Shiver Flak Vest.png", + "system": { + "description": "Standard-issue soft armour vest worn under uniform by Shivers and patrol officers. Lightweight, concealable under a jacket. Not rated for combat \u2014 rated for surviving a mugging.", + "armorRating": 1, + "armorDice": 1, + "armorAuto": 0, + "statMod": 0, + "skillMod": 0, + "statModTarget": "", + "skillModTarget": "", + "cost": 200, + "initiativeMod": 0 + } + }, + { + "name": "Shock Armour", + "type": "armor", + "img": "systems/slazero2/assets/SLA_Assets/Armor/2nd try/Shock Armor.png", + "system": { + "description": "Blast-resistant armour with energy-dispersal panels. Designed for operatives who expect to be in the vicinity of explosions, EMP discharges, or Ebb detonations. Reduces Ebb damage by 1 auto.", + "armorRating": 3, + "armorDice": 3, + "armorAuto": 1, + "statMod": 0, + "skillMod": 0, + "statModTarget": "", + "skillModTarget": "", + "cost": 900, + "initiativeMod": 0 + } + }, + { + "name": "Squad Shielded Power Armor", + "type": "armor", + "img": "systems/slazero2/assets/SLA_Assets/Armor/Squad Shieled Power Armor.png", + "system": { + "description": "Full sealed power armor with integrated energy shielding; rare, expensive, and terrifying on the street.", + "armorRating": 6, + "armorDice": 6, + "armorAuto": 3, + "statMod": 1, + "skillMod": -2, + "statModTarget": "strength", + "skillModTarget": "stealth", + "cost": 8000, + "initiativeMod": 0 + } + }, + { + "name": "Stealth Suit", + "type": "armor", + "img": "systems/slazero2/assets/SLA_Assets/Armor/2nd try/Stealth Suit.png", + "system": { + "description": "A full-body suit with integrated light-bending panels and sound-dampening underlayer. Provides moderate ballistic protection while granting +2 dice to Stealth rolls. Panels degrade when the suit takes significant damage.", + "armorRating": 2, + "armorDice": 2, + "armorAuto": 0, + "statMod": 0, + "skillMod": 2, + "statModTarget": "", + "skillModTarget": "stealth", + "cost": 2500, + "initiativeMod": 0 + } + }, + { + "name": "Street Clothes", + "type": "armor", + "img": "systems/slazero2/assets/SLA_Assets/Armor/2nd try/Street Cloths.png", + "system": { + "description": "Civilian clothing. Offers essentially no ballistic protection. The only reason to track it is so the GM knows when an operative has forgotten to put their armour on.", + "armorRating": 0, + "armorDice": 0, + "armorAuto": 0, + "statMod": 0, + "skillMod": 0, + "statModTarget": "", + "skillModTarget": "", + "cost": 20, + "initiativeMod": 0 + } + } +] \ No newline at end of file diff --git a/packs/blade-runner-rpg-system.db b/packs/blade-runner-rpg-system.db new file mode 100644 index 0000000..e4b3c6d --- /dev/null +++ b/packs/blade-runner-rpg-system.db @@ -0,0 +1 @@ +{"name":"Game System's Manual","pages":[{"sort":200000,"name":"Welcome","type":"text","_id":"VCkEkNYk4WDFv2h2","title":{"show":false,"level":1},"image":{},"text":{"format":1,"content":"","markdown":""},"video":{"controls":true,"volume":0.5},"src":null,"system":{},"ownership":{"default":-1},"flags":{}},{"sort":300000,"name":"Actors","type":"text","_id":"86fJk4DI7fdzlEXT","title":{"show":true,"level":1},"image":{},"text":{"format":1,"content":"

Existing Actor Types

There is only one type of actors:

  • @UUID[.pRLrJILq6IQiNIWi]{Characters}: character
    It is further subdivided into two categories:

    • Player Character (PC)
    • Non-Player Character (NPC)
  • @UUID[.qT3HaImubPpZnwBS]{Vehicles}: vehicle

  • Item Piles (Loot): loot
    Used to only store inventory.

","markdown":""},"video":{"controls":true,"volume":0.5},"src":null,"system":{},"ownership":{"default":-1},"flags":{}},{"sort":400000,"name":"Rolling The Dice","type":"text","_id":"uhUJqSgCPRkMz9rq","title":{"show":true,"level":1},"image":{},"text":{"format":1,"content":"

The Roll Dialog

The Roll Dialog (see picture below) is the main interface for rolling the dice. It is displayed by doing one of the following:

  • Clicking a skill or attribute in the character sheet's @FontAwesomeIcon[fas fa-horse-head] Stats tab.

  • Clicking an action in the character sheet's @FontAwesomeIcon[fas fa-fist-raised] Combat tab.

  • Clicking a rollable @FontAwesomeIcon[fas fa-suitcase] Item.

  • Clicking the Generic Roll @FontAwesomeIcon[fas fa-dice] button in the character sheet's header.

  • Clicking an [[/r 1d12p1]]{inline roll}.

  • Executing a script @FontAwesomeIcon[fas fa-code] @UUID[.DxIFpHTNsLBzI93V]{Macro} with either of the following codes:

// Generic Roll\ngame.bladerunner.macros.rollDice();\n\n// Roll Application\ngame.bladerunner.roller.create(options);

The Roll Dialog is divided into four sections: the dice (top), the modifiers (left), the settings (right) and the buttons (bottom) —

Dice

The top area of the Roll Dialog displays the dice that will be rolled. By default, it will choose the dice of your linked skill and attribute.

You can click the plus @FontAwesomeIcon[fa fa-plus] icon to add an extra die. A maximum of 3 dice is allowed.

Modifiers

The left area lists all possible modifiers from the character's Items. Click the checkbox to enable/disable a modifier. If you activate more than one modifier, the system will consider the average. Ranged attacks have an additional set of modifiers.

Settings

The right area is where you can set the maximum number of pushes and the default roll mode for the chat message.

Buttons

The Roll Dialog has three buttons to choose:

  • Advantage: Adds an extra die with a size equal to the lowest of the other dice, then rolls the dice.

  • Disadvantage: Removes the lowest die, then rolls.

  • Roll: Rolls the dice as normal.

The buttons can be greyed out following the number of dice or the chosen modifiers.

Pushing the Roll

The result of a roll is sent as a message in the chat log. Click the formula to reveal the dice results individually.

If the roll is pushable, two buttons will be displayed on the chat message:

  • Push: Click this button to push the roll.

  • Cancel Push: Click this button to accept the current result and remove the push possibility.

Applying Damage

It is possible to automatically apply the damage from a weapon roll:

  1. Roll a weapon that can inflict damage.

  2. Target one or more tokens. (Click the bullseye @FontAwesomeIcon[fas fa-bullseye] icon.)

  3. Right-click the roll message. If the roll is successful and you targeted at least one token, the \"@FontAwesomeIcon[fas fa-crosshairs] Apply damage to targeted tokens\" option is available. Click it.

  4. If you rolled more than one success and targeted multiple tokens, choose how to spread the successes between the targets.

  5. If the target has an armor, choose whether you want to roll it. Successes from the armor roll decreased the damage.

The Actors's Health is automatically adjusted and a resume message is sent to the chat log.

Note: Only the GM can apply damage.

","markdown":""},"video":{"controls":true,"volume":0.5},"src":null,"system":{},"ownership":{"default":-1},"flags":{}},{"sort":350000,"name":"Items","type":"text","_id":"lVEw83SdtQBh8lsa","title":{"show":true,"level":1},"image":{},"text":{"format":1,"content":"

Existing Item Types

There are 7 types of items:

  • Generic: generic — General equipment that don't fall in any of the types below.

  • Augment: upgrade — Synthetic augmentations that upgrades the character's body.

  • Armor: armor — Pieces of protection.

  • Weapon: weapon — Close-combat and ranged weapons, both for characters and vehicles.

  • Explosive: explosive — Grenades, explosives and smoke bombs.

  • Specialty: specialty — Character specialties.

  • Injury: injury — Critical injuries and stress effects.

Items are draggable. Drag'n drop an Item into an Actor sheet to create a copy in the Actor's inventory. You can also drag'n drop an Item into the Macro Hotbar to create a roll macro.

Sheet header buttons

Like the Actor sheet, the header bar of the Item sheet contains a series of controls:

  • @FontAwesomeIcon[fa-solid fa-passport] Copy the Item's ID to the clipboard

  • @FontAwesomeIcon[fas fa-bomb] Roll the Blast Power (only displayed on an Explosive item)

  • @FontAwesomeIcon[far fa-comment-dots] Send a chat message detailing the item.

  • @FontAwesomeIcon[fas fa-cog] Item sheet configuration

  •  @FontAwesomeIcon[fas fa-times]  Close the sheet

@FontAwesomeIcon[fas fa-align-left] Info (tab)

This tab hosts the item's description.

@FontAwesomeIcon[fas fa-briefcase] Features (tab)

This tab contains all the properties and features of the item.

There is also a Modifiers section within certain item types where you can define one or more modifiers caused by the item. Click the @FontAwesomeIcon[fas fa-plus] button to add a new modifier, choose the affected attribute or skill, and choose between Advantage or Disadvantage.

@FontAwesomeIcon[fas fa-dice-six] Actions (tab)

Some item types have also an @FontAwesomeIcon[fas fa-dice-six] Actions tab. Click the @FontAwesomeIcon[fas fa-plus] button to add an item action and choose the attribute and skill used to roll it, or specify the name or id of a macro to execute.

","markdown":""},"video":{"controls":true,"volume":0.5},"src":null,"system":{},"ownership":{"default":-1},"flags":{}},{"sort":500000,"name":"Playing With Macros","type":"text","_id":"DxIFpHTNsLBzI93V","title":{"show":true,"level":1},"image":{},"text":{"format":1,"content":"

The Blade Runner system comes with a few macros that can help you run the game smoothly. They can be found in the @FontAwesomeIcon[fas fa-folder] Blade Runner Macros folder.

  • @IconImage[modules/blade-runner-core-game/assets/icons/magnifying_glass.webp] Generate a Case File: Sends a message with a random Case File generated from the provided tables.

  • @IconImage[icons/svg/dice-target.svg] Roll: Displays the @UUID[.uhUJqSgCPRkMz9rq#the-roll-dialog]{Roll Dialog}

If you want to create your own macro, you can use the available async methods:

  • game.bladerunner.macros.rollDice(); — Displays the Roll Dialog.

  • game.bladerunner.roller.create(options); — Same, but with additional options. See example below:

    await game.bladerunner.roller.create({\n  title: 'Generic Roll',\n  actor: game.user.character,\n  attributeKey: null,\n  skillKey: null,\n  dice: [6, 12],\n  maxPush: 1\n});
  • game.bladerunner.macros.rollItem(itemName); — Rolls an item based on its name (must be embedded in a character).
  • game.bladerunner.macros.rollStat(attributeKey, skillKey); — Rolls a stat of the character.
  • game.bladerunner.macros.rollAction(actionName); — Performs an action for the character. See game.bladerunner.actions.

RollData

Attributes and Skills' values can be easily accessed with the @ tag:

ATTRIBUTES
@ + attributeKey

SKILLS
@ + skillKey

OTHER
@ + something

@str@closeCombat@maxPush
 @force 
 @stamina@health.value
@agi@firearms@resolve.value
 @mobilityetc...
 @stealth 
@int@medicalAid 
 @observation 
 @tech 
@emp@connections 
 @insight 
 @manipulation 

You can use these tags to get the value of a skill for custom rolls in the chat. For example, to roll for TECH, type the following:

/r 1d@int + 1d@tech

But this example does not output a roll with the Year Zero template. To get the right template, you must define the maximum number of pushes with the modifier p, like so:

/r 1d(@int)p1 + 1d(@tech)p1

To get the maximum number of pushes from the character's nature (for example, it is 2 for Replicants), you can use the @maxPush tagged property:

/r 1d(@int)p@maxPush + 1d(@tech)p@maxPush

If you want to disable the push, use p0 or np instead:

/r 1d(@int)np + 1d(@tech)np

Another example for rolling an explosive's Blast Power:

/r 2d(@blast)np

Custom Text Enrichers

The Blade Runner system also defines a few additional custom Text Enrichers. They can be used in any text editor, like in the character's biography, in Journal entries, and in chat messages.

  • Add a button that draws a table and displays the result in the chat when clicked:
    Example: [[/t Signature Item]]{Click me to draw a Signature item}

[[⁠/table My Table]]{Custom title}
  • Draw the result of a table and display it in the text:
    Example: [[table: Signature Item]]{Signature item}

[[⁠table: My Table]]{Custom title}\n[[⁠table: My Table]]{Custom title}{2}
  • Choose a random result among a list of choices:
    [[choose: 1d3 #Choice 1|Choice 2|Choice 3]]{Example}

[[⁠choose: 1d3 #Choice 1|Choice 2|Choice 3]]{Choice title}
  • Create a Font Awesome icon:
@⁠FontAwesomeIcon[fas fa-cog]
  • Create a Blade Runner symbol [[S]] or [[F]]:
[[⁠S]] or [[⁠F]]
  • Create a description section for an Actor:
    Note: the Actor's stats and gear are not displayed when used in a chat message.

@⁠BladeRunnerActor[Name or ID]

And many more... See the src/system/enricher.js file in the Github repository for more supported text enrichers.

","markdown":""},"video":{"controls":true,"volume":0.5},"src":null,"system":{},"ownership":{"default":-1},"flags":{}},{"sort":250000,"name":"Instructions","type":"text","_id":"7QtXCWimDMVof31e","title":{"show":true,"level":1},"image":{},"text":{"format":1,"content":"

RTFM!

We know you are eager to play! But you should definitely read this manual first. The pages in this Journal entry will explain you the basics to efficiently use this game system.

  • @UUID[.86fJk4DI7fdzlEXT]: How to use the Actor sheets as well as the tokens.

  • @UUID[.lVEw83SdtQBh8lsa]: How to use the Item sheet and the different item types.

  • @UUID[.uhUJqSgCPRkMz9rq]: How to roll the dice and push them.

  • @UUID[.lKQwDtVBwLeeUk0y]: How to use the combat and draw the initiative cards.

  • @UUID[.DxIFpHTNsLBzI93V]: How to ease your game. (For advanced players.)

  • @UUID[.zio6bsCF9tr0XlAn]: List of recommended modules and other third-party addons.

","markdown":""},"video":{"controls":true,"volume":0.5},"src":null,"system":{},"ownership":{"default":-1},"flags":{}},{"sort":600000,"name":"Third-party","type":"text","_id":"zio6bsCF9tr0XlAn","title":{"show":true,"level":1},"image":{},"text":{"format":1,"content":"

List of Recommended Modules

","markdown":""},"video":{"controls":true,"volume":0.5},"src":null,"system":{},"ownership":{"default":-1},"flags":{}},{"sort":450000,"name":"Combat","type":"text","_id":"lKQwDtVBwLeeUk0y","title":{"show":true,"level":1},"image":{},"text":{"format":1,"content":"

Drawing Initiative

This game system use a @FontAwesomeIcon[fa-solid fa-dice-d10] D10 for initiative, with Agility as a tie-breaker. To use the initiative cards, you must install the Year Zero Engine: Combat module.

The Starter Set premium module also includes a set of predefined Initiative cards that you can use with that module.

","markdown":""},"video":{"controls":true,"volume":0.5},"src":null,"system":{},"ownership":{"default":-1},"flags":{}},{"sort":306250,"name":"Vehicles","type":"text","_id":"qT3HaImubPpZnwBS","title":{"show":true,"level":2},"image":{},"text":{"format":1,"content":"

The vehicle sheet shares many similarities with the @UUID[.pRLrJILq6IQiNIWi]{Character sheet}. Read also this section for additional information.

Hull

The Hull is the life gauge of the vehicle. By default, a newly created vehicle starts with 2 hull points. You can change the maximum hull value by right-clicking on the \"Hull\" title or the numbers to display a context menu. The current value is editable like in the @UUID[.pRLrJILq6IQiNIWi#health-and-resolve]{character sheet} (left/right-click on the boxes).

Crew

The vehicle's crew is managed in the crew section of the vehicle sheet. Click the @FontAwesomeIcon[fas fa-plus] button to add an extra seat, or right-click a seat to display a context menu with additional options:

  • @FontAwesomeIcon[fas fa-edit] Edit Passenger: display the passenger's character sheet.

  • @FontAwesomeIcon[fas fa-trash] Remove Passenger: free the seat.

  • @FontAwesomeIcon[fas fa-times] Decrease Passenger Capacity: remove the last seat.

To add a crew member, drag'n drop a Character from the sidebar into the vehicle sheet. If that character is an NPC, the game system asks if you want to duplicate it (because if you want multiple similar NPCs inside the same vehicle, you must duplicate them). Afterwards, that character becomes linked into the vehicle and its avatar appears in the crew section (like a token on the canvas) with attribute bars displayed if relevant.

The first crew member is considered to be the driver. You can drag'n drop passengers inside the crew section to change their position.

Finally, dragging a crew member from the vehicle sheet into the canvas will create a token of that linked character. (But this isn't working the other way around.)

Note: It is unfortunately not possible to crew other vehicles inside a vehicle. Vehicles inceptions are not allowed!

 

Inventories

The vehicle's items are managed the same way as in the @UUID[.pRLrJILq6IQiNIWi#inventories]{character sheet}. To add a new item, drag'n drop it onto the vehicle sheet.

The hover controls may have two additional icons:

  • @FontAwesomeIcon[fas fa-wrench] Mount the weapon and move it to the @FontAwesomeIcon[fa-regular fa-crosshairs-simple] Combat tab. (Only damaging weapons can be mounted.)

  • @FontAwesomeIcon[fas fa-thumbtack] Unmount the weapon. (And move it back to the inventory tab.)

@FontAwesomeIcon[fa-regular fa-crosshairs-simple] Combat (tab)

This tab contains all your combat-related items and a list of some Actions shortcuts.

  • Mounted Weapons: List of all the mounted weapons on the vehicle ready to be fired. Use the controls described above to mount/unmount a weapon. When using a mounted weapon, you are first prompted with a dialog to select the vehicle's occupant that will fire the weapon.

  • Altitude: An integer value to define at which altitude the vehicle is currently flying. Used only by a few vehicle actions (see below).

  • Components: List of all vehicle's components. Click the toggle button to mark it disabled. Currently, only the engine is supported, you cannot add more components. Except (for advanced users) by modifying the system.components array property in the vehicle's data.

  • Actions: See below.

Actions

This box contains a list of shortcuts for common vehicle actions, like speeding, ramming and repairing (see @UUID[JournalEntry.tDbakf6dnhYJtfHp.JournalEntryPage.CZYUmCgo6Ttxggym#vehicles-in-combat]{Vehicle Combat} for more details on these actions).

Clicking an action will execute a predefined macro. If the action involves a skill roll and the vehicle has more than one crew member, the game system asks which occupant to choose to perform the roll.

Crash, Massive Crash and Explode inflict damage to the crew members and decrease the health of their linked characters accordingly.

Note for module developers: See the game.bladerunner.config.Actions configuration variable to add your own custom actions.

","markdown":""},"video":{"controls":true,"volume":0.5},"src":null,"system":{},"ownership":{"default":-1},"flags":{}},{"sort":312500,"name":"Tokens","type":"text","_id":"8cDPxQsP0ITHHHBc","title":{"show":false,"level":2},"image":{},"text":{"format":1,"content":"

Tokens

When an actor is dragged onto a Scene, a token is created. It displays one or two resource bars and the token's name when it is hovered.

The default Prototype Token for a PC has two bars:

  • The upper bar (blue) tracks the character's Resolve

  • The lower bar (green) tracks the character's Health

NPC tokens have only the Health bar displayed by default.

By default, the tokens are unlinked and their disposition is set to hostile.

","markdown":""},"video":{"controls":true,"volume":0.5},"src":null,"system":{},"ownership":{"default":-1},"flags":{}},{"sort":303125,"name":"Characters","type":"text","_id":"pRLrJILq6IQiNIWi","title":{"show":true,"level":2},"image":{},"text":{"format":1,"content":"

Sheet header buttons

The header bar of the character sheet contains the following icons:

  • @FontAwesomeIcon[fa-solid fa-passport] Copy the Actor's ID to the clipboard

  • @FontAwesomeIcon[fas fa-dice] Shortcut for the roll dialog

  • @FontAwesomeIcon[fas fa-cog] @UUID[.pRLrJILq6IQiNIWi#actor-sheet-configuration]{Actor sheet configuration}

  • @FontAwesomeIcon[fas fa-user-circle] @UUID[.8cDPxQsP0ITHHHBc]{Default token's configuration}

  •  @FontAwesomeIcon[fas fa-times]  Close the sheet

Nature

The dropdown allows to choose between the existing natures: Human or Replicant. Changing the nature has effects on the health, resolve and how rolls are pushed.

Note: Only the GM can change this field.

Health & Resolve

Those are the character's capacities.

  • Left-click: increase the current value
  • Right-click: decrease the current value

A greyed box @FontAwesomeIcon[far fa-minus-square] indicates a point permanently lost.

The maximums are automatically calculated from the character's nature and attributes. They can be further modified by the GM in the @UUID[.pRLrJILq6IQiNIWi#actor-sheet-configuration]{Actor sheet configuration}.

The Resolve capacity is not displayed on a Non-Player Character sheet. The tracking of stress for generic NPCs is usually skipped, as per the rules. If the GM wants to track stress for uncommon NPCs, she should use a Player Character sheet instead.

When the Resolve capacity reaches 0, it triggers a test for @UUID[JournalEntry.tDbakf6dnhYJtfHp.JournalEntryPage.cPRYuc5giAQGFH4P#permanent-loss-of-resolve]{Permanent Loss of Resolve}.

Archetype

The field for the character's archetype is located on the right side of the header. Click on the empty space and choose a predefined archetype or write your own. The choice of an archetype is currently only cosmetic.

Inventories

In many places in the character sheet, there are inventories for the items embedded in the character.

To add a new item, you can drag'n drop it onto the character sheet, or click the plus @FontAwesomeIcon[fas fa-plus] on the side the create a new one from scratch.

Hover the embedded item to show the control icons:

  • @FontAwesomeIcon[fas fa-dice-d20] Roll the item, if it's rollable. (A rollable item has also a golden halo around its picture. You can click the picture of a rollable item to trigger the roll dialog.)

  • @FontAwesomeIcon[fas fa-edit] Edit the item. (You can also right-click the item to edit it.)

  • @FontAwesomeIcon[far fa-comment-dots] Create a chat message with item's details. (The message's privacy uses your choice from the Default Roll Mode dropdown.)

  • @FontAwesomeIcon[fas fa-bomb] Make a Blast Power roll. (Only for Explosive items.)

  • @FontAwesomeIcon[fas fa-shield-alt] Make an Armor roll. (Only for Armor items.)

  •  @FontAwesomeIcon[fas fa-times]  Delete the item.

@FontAwesomeIcon[fas fa-horse-head] Stats (tab)

This tab lists all the character's attributes and skills. Use the dropdown menu to choose the stat's level and click on the stat's name or base die to trigger the roll dialog.

Also, the stat's name is draggable. Drag'n drop it into the Macro Hotbar to create a roll macro!

@FontAwesomeIcon[fas fa-puzzle-piece] Mods (tab)

This tab contains all Specialties and Synthetic Augmentations.

@FontAwesomeIcon[fas fa-fist-raised] Combat (tab)

This tab contains all your combat-related items and a list of some Actions shortcuts. Clicking an action triggers a roll dialog with the defined attribute and skill.

Also, the action's name is draggable. Drag'n drop it into the Macro Hotbar to create a macro!

Note for module developers: See the game.bladerunner.config.Actions configuration variable to add your own custom actions.

@FontAwesomeIcon[fas fa-archive] Inventory (tab)

This tab contains a field for the character's Signature Item, an inventory for Generic items, and boxes for tracking multiple currencies: Promotion, Humanity and Chinyen points.

Like for the Health and Resolve capacities, you can left- or right-click the boxes to modify their quantities.

@FontAwesomeIcon[fas fa-align-left] Biography (tab)

This tab contains multiple Text Editor fields to use for the character biography.
NPC sheets only have the \"Notes\" field.

Actor sheet configuration

  • Subtype: Use the dropdown to select between the character subtypes Player Character or Non-Player Character. The later provides a pared down character sheet.

  • Health & Resolve max. modifiers: Use these fields to modify the character's health and/or resolve. (Only the GM can change these values.)

  • Resolve permanent loss: Sets the permanent resolve losses. (Only the GM can change this value.)

  • Vehicle maneuverability: Sets the vehicle's Maneuverability attribute.

  • Allow unlimited pushed rolls: Whether to allow an unlimited number of pushes for this character's rolls. (Note: Replicant characters already have a max. pushes set to 2.)

","markdown":""},"video":{"controls":true,"volume":0.5},"src":null,"system":{},"ownership":{"default":-1},"flags":{}}],"flags":{"core":{"sourceId":"JournalEntry.uflJFO3xEsjM4XgQ"}},"_stats":{"systemId":"blade-runner","systemVersion":"10.1.0","coreVersion":"10.303","createdTime":1665913290950,"modifiedTime":1687812078155,"lastModifiedBy":"QCdXtTE3VPASp0bb"},"folder":null,"sort":0,"ownership":{"default":0,"QCdXtTE3VPASp0bb":3},"_id":"r5B5wsMCKGbH6IJ1"} diff --git a/packs/blade-runner-rpg-system/000004.log b/packs/blade-runner-rpg-system/000004.log new file mode 100644 index 0000000..e69de29 diff --git a/packs/blade-runner-rpg-system/000005.ldb b/packs/blade-runner-rpg-system/000005.ldb new file mode 100644 index 0000000..5876769 Binary files /dev/null and b/packs/blade-runner-rpg-system/000005.ldb differ diff --git a/packs/blade-runner-rpg-system/CURRENT b/packs/blade-runner-rpg-system/CURRENT new file mode 100644 index 0000000..1a84852 --- /dev/null +++ b/packs/blade-runner-rpg-system/CURRENT @@ -0,0 +1 @@ +MANIFEST-000002 diff --git a/packs/blade-runner-rpg-system/LOG b/packs/blade-runner-rpg-system/LOG new file mode 100644 index 0000000..17da443 --- /dev/null +++ b/packs/blade-runner-rpg-system/LOG @@ -0,0 +1,5 @@ +2026/02/03-16:19:47.453295 30a642000 Delete type=3 #1 +2026/02/03-16:45:37.355237 3045b3000 Level-0 table #5: started +2026/02/03-16:45:37.355867 3045b3000 Level-0 table #5: 35355 bytes OK +2026/02/03-16:45:37.356079 3045b3000 Delete type=0 #3 +2026/02/03-16:45:37.358525 3045b3000 Manual compaction at level-0 from '!journal!r5B5wsMCKGbH6IJ1' @ 72057594037927935 : 1 .. '!journal.pages!r5B5wsMCKGbH6IJ1.zio6bsCF9tr0XlAn' @ 0 : 0; will stop at (end) diff --git a/packs/blade-runner-rpg-system/MANIFEST-000002 b/packs/blade-runner-rpg-system/MANIFEST-000002 new file mode 100644 index 0000000..1c6cac0 Binary files /dev/null and b/packs/blade-runner-rpg-system/MANIFEST-000002 differ diff --git a/packs/drugs.json b/packs/drugs.json new file mode 100644 index 0000000..8d65e0f --- /dev/null +++ b/packs/drugs.json @@ -0,0 +1,1229 @@ +[ + { + "name": "Painkillers (Analgesic)", + "type": "equipment", + "img": "systems/slazero2/assets/SLA_Assets/Drugs/Painkiller.png", + "system": { + "description": "Prescription-grade painkillers used by Medics to keep operatives moving on broken bones and bullet wounds.", + "isDrug": true, + "quantity": 1, + "cost": 30, + "active": false, + "activeDuration": "1 shift", + "activeLabel": "", + "activeNotes": "", + "withdrawalActive": false, + "withdrawalDuration": "1-3 shifts", + "withdrawalLabel": "", + "withdrawalNotes": "Heavy use only (e.g. daily for a week).", + "healthMod": 0, + "resolveMod": 1, + "statPhysicalMod": 0, + "skillAllMod": 0, + "panicReduction": 0, + "injuryPenaltyIgnore": 2, + "stressRecoveryBonus": 0, + "postUseStaminaDamage": 0, + "wdHealthMod": 0, + "wdResolveMod": -1, + "wdStatPhysicalMod": 0, + "wdSkillAllMod": 0, + "wdPanicReduction": 0, + "wdInjuryPenaltyIgnore": 0, + "wdStressRecoveryBonus": 0, + "wdPostUseStaminaDamage": 0, + "skillMods": { + "force": 0, + "melee": 0, + "stamina": 0, + "marksmanship": 0, + "mobility": 0, + "stealth": 0, + "crafting": 0, + "observation": 0, + "survival": 0, + "healing": 0, + "insight": 0, + "persuasion": 0 + }, + "wdSkillMods": { + "force": 0, + "melee": 0, + "stamina": 0, + "marksmanship": 0, + "mobility": 0, + "stealth": 0, + "crafting": -1, + "observation": -1, + "survival": 0, + "healing": 0, + "insight": 0, + "persuasion": 0 + } + } + }, + { + "name": "Stimulant (Focus/Wake-up)", + "type": "equipment", + "img": "systems/slazero2/assets/SLA_Assets/Drugs/Stimulant.png", + "system": { + "description": "Corporate-grade stimulant chips; keep you awake, sharp, and moving long after your body wants to shut down.", + "isDrug": true, + "quantity": 1, + "cost": 40, + "active": false, + "activeDuration": "1 shift (2-4 hours)", + "activeLabel": "", + "activeNotes": "", + "withdrawalActive": false, + "withdrawalDuration": "Next shift", + "withdrawalLabel": "", + "withdrawalNotes": "", + "healthMod": 0, + "resolveMod": 1, + "statPhysicalMod": 1, + "skillAllMod": 0, + "panicReduction": 0, + "injuryPenaltyIgnore": 0, + "stressRecoveryBonus": 0, + "postUseStaminaDamage": 0, + "wdHealthMod": 0, + "wdResolveMod": -1, + "wdStatPhysicalMod": -1, + "wdSkillAllMod": -1, + "wdPanicReduction": 0, + "wdInjuryPenaltyIgnore": 0, + "wdStressRecoveryBonus": 0, + "wdPostUseStaminaDamage": 0, + "skillMods": { + "force": 0, + "melee": 0, + "stamina": 0, + "marksmanship": 0, + "mobility": 0, + "stealth": 0, + "crafting": 1, + "observation": 1, + "survival": 0, + "healing": 0, + "insight": 0, + "persuasion": 0 + }, + "wdSkillMods": { + "force": 0, + "melee": 0, + "stamina": 0, + "marksmanship": 0, + "mobility": 0, + "stealth": 0, + "crafting": 0, + "observation": 0, + "survival": 0, + "healing": 0, + "insight": 0, + "persuasion": 0 + } + } + }, + { + "name": "Sedative (Downer)", + "type": "equipment", + "img": "systems/slazero2/assets/SLA_Assets/Drugs/Sedative.png", + "system": { + "description": "Heavy sedative used to shut down panic, control Frothers, or help Ebons come down after bad Ebb feedback.", + "isDrug": true, + "quantity": 1, + "cost": 35, + "active": false, + "activeDuration": "1 stretch to 1 shift", + "activeLabel": "", + "activeNotes": "Recover +2 extra Stress once when taken.", + "withdrawalActive": false, + "withdrawalDuration": "1 shift", + "withdrawalLabel": "", + "withdrawalNotes": "Abuse only (daily for a week).", + "healthMod": 0, + "resolveMod": 2, + "statPhysicalMod": 0, + "skillAllMod": -1, + "panicReduction": 0, + "injuryPenaltyIgnore": 0, + "stressRecoveryBonus": 2, + "postUseStaminaDamage": 0, + "wdHealthMod": 0, + "wdResolveMod": -1, + "wdStatPhysicalMod": 0, + "wdSkillAllMod": 0, + "wdPanicReduction": 0, + "wdInjuryPenaltyIgnore": 0, + "wdStressRecoveryBonus": 0, + "wdPostUseStaminaDamage": 0, + "skillMods": { + "force": 0, + "melee": 0, + "stamina": 0, + "marksmanship": 0, + "mobility": 0, + "stealth": 0, + "crafting": 0, + "observation": 0, + "survival": 0, + "healing": 0, + "insight": 0, + "persuasion": 0 + }, + "wdSkillMods": { + "force": 0, + "melee": 0, + "stamina": 0, + "marksmanship": 0, + "mobility": 0, + "stealth": 0, + "crafting": 0, + "observation": 0, + "survival": 0, + "healing": 0, + "insight": 0, + "persuasion": 0 + } + } + }, + { + "name": "Redline (Overclock Combat Booster)", + "type": "equipment", + "img": "systems/slazero2/assets/SLA_Assets/Drugs/Redline.png", + "system": { + "description": "Illegal DarkNight derivative of Rush; pushes the body past safe limits into true overdrive.", + "isDrug": true, + "quantity": 1, + "cost": 110, + "active": false, + "activeDuration": "1 scene/combat (max 1 stretch)", + "activeLabel": "", + "activeNotes": "", + "withdrawalActive": false, + "withdrawalDuration": "1 shift", + "withdrawalLabel": "", + "withdrawalNotes": "Make a Stamina test; on failure take 1 damage ignoring armor.", + "healthMod": 1, + "resolveMod": -2, + "statPhysicalMod": 2, + "skillAllMod": 0, + "panicReduction": 0, + "injuryPenaltyIgnore": 0, + "stressRecoveryBonus": 0, + "postUseStaminaDamage": 0, + "wdHealthMod": -1, + "wdResolveMod": 0, + "wdStatPhysicalMod": -1, + "wdSkillAllMod": 0, + "wdPanicReduction": 0, + "wdInjuryPenaltyIgnore": 0, + "wdStressRecoveryBonus": 0, + "wdPostUseStaminaDamage": 1, + "skillMods": { + "force": 0, + "melee": 1, + "stamina": 0, + "marksmanship": 1, + "mobility": 0, + "stealth": 0, + "crafting": 0, + "observation": 0, + "survival": 0, + "healing": 0, + "insight": 0, + "persuasion": 0 + }, + "wdSkillMods": { + "force": 0, + "melee": 0, + "stamina": 0, + "marksmanship": 0, + "mobility": 0, + "stealth": 0, + "crafting": 0, + "observation": 0, + "survival": 0, + "healing": 0, + "insight": 0, + "persuasion": 0 + } + } + }, + { + "name": "Calm-Line (Anti-Panic Neuroregulator)", + "type": "equipment", + "img": "systems/slazero2/assets/SLA_Assets/Drugs/Calmline.png", + "system": { + "description": "Corporate anti-anxiety drug; standard issue for some Shiver units and corporate VIPs who still have to go on-site.", + "isDrug": true, + "quantity": 1, + "cost": 65, + "active": false, + "activeDuration": "1 shift", + "activeLabel": "", + "activeNotes": "", + "withdrawalActive": false, + "withdrawalDuration": "1 stretch to 1 shift", + "withdrawalLabel": "", + "withdrawalNotes": "Heavy/repeated use only.", + "healthMod": 0, + "resolveMod": 2, + "statPhysicalMod": 0, + "skillAllMod": 0, + "panicReduction": 2, + "injuryPenaltyIgnore": 0, + "stressRecoveryBonus": 0, + "postUseStaminaDamage": 0, + "wdHealthMod": 0, + "wdResolveMod": -1, + "wdStatPhysicalMod": 0, + "wdSkillAllMod": 0, + "wdPanicReduction": 0, + "wdInjuryPenaltyIgnore": 0, + "wdStressRecoveryBonus": 0, + "wdPostUseStaminaDamage": 0, + "skillMods": { + "force": 0, + "melee": 0, + "stamina": 0, + "marksmanship": 0, + "mobility": 0, + "stealth": 0, + "crafting": 0, + "observation": 0, + "survival": 0, + "healing": 0, + "insight": -1, + "persuasion": -1 + }, + "wdSkillMods": { + "force": 0, + "melee": 0, + "stamina": 0, + "marksmanship": 0, + "mobility": 0, + "stealth": 0, + "crafting": 0, + "observation": 0, + "survival": 0, + "healing": 0, + "insight": 0, + "persuasion": 0 + } + } + }, + { + "name": "Rush (Combat Stimulant)", + "type": "equipment", + "img": "systems/slazero2/assets/SLA_Assets/Drugs/Rush.png", + "system": { + "description": "Standard Frother combat drug; a violent mix of stimulants, painkillers, and combat enhancers widely available on Mort.", + "isDrug": true, + "quantity": 1, + "cost": 85, + "active": false, + "activeDuration": "1 scene/combat (up to 1 stretch)", + "activeLabel": "", + "activeNotes": "", + "withdrawalActive": false, + "withdrawalDuration": "1 stretch", + "withdrawalLabel": "", + "withdrawalNotes": "Daily use for a week risks addiction.", + "healthMod": 1, + "resolveMod": -1, + "statPhysicalMod": 1, + "skillAllMod": 0, + "panicReduction": 0, + "injuryPenaltyIgnore": 0, + "stressRecoveryBonus": 0, + "postUseStaminaDamage": 0, + "wdHealthMod": 0, + "wdResolveMod": -1, + "wdStatPhysicalMod": -1, + "wdSkillAllMod": 0, + "wdPanicReduction": 0, + "wdInjuryPenaltyIgnore": 0, + "wdStressRecoveryBonus": 0, + "wdPostUseStaminaDamage": 0, + "skillMods": { + "force": 0, + "melee": 1, + "stamina": 0, + "marksmanship": 1, + "mobility": 0, + "stealth": 0, + "crafting": 0, + "observation": 0, + "survival": 0, + "healing": 0, + "insight": 0, + "persuasion": 0 + }, + "wdSkillMods": { + "force": -1, + "melee": -1, + "stamina": -1, + "marksmanship": -1, + "mobility": 0, + "stealth": 0, + "crafting": 0, + "observation": 0, + "survival": 0, + "healing": 0, + "insight": 0, + "persuasion": 0 + } + } + }, + { + "name": "UV (Ultra-Violence Combat Drug)", + "type": "equipment", + "img": "systems/slazero2/assets/SLA_Assets/Drugs/UV.png", + "system": { + "description": "Restricted UV-grade combat cocktail reserved for black ops and suicidal Frothers; massively amplifies aggression and physical output at severe mental cost.", + "isDrug": true, + "quantity": 1, + "cost": 200, + "active": false, + "activeDuration": "1 scene/combat (max 1 stretch)", + "activeLabel": "", + "activeNotes": "GM may end early on catastrophic Panic.", + "withdrawalActive": false, + "withdrawalDuration": "1 shift", + "withdrawalLabel": "", + "withdrawalNotes": "After each dose, roll Stamina; on failure take 2 damage ignoring armor.", + "healthMod": 2, + "resolveMod": -3, + "statPhysicalMod": 2, + "skillAllMod": 0, + "panicReduction": 0, + "injuryPenaltyIgnore": 0, + "stressRecoveryBonus": 0, + "postUseStaminaDamage": 0, + "wdHealthMod": -1, + "wdResolveMod": -2, + "wdStatPhysicalMod": -2, + "wdSkillAllMod": -2, + "wdPanicReduction": 0, + "wdInjuryPenaltyIgnore": 0, + "wdStressRecoveryBonus": 0, + "wdPostUseStaminaDamage": 2, + "skillMods": { + "force": 0, + "melee": 2, + "stamina": 0, + "marksmanship": 2, + "mobility": 0, + "stealth": 0, + "crafting": 0, + "observation": 0, + "survival": 0, + "healing": 0, + "insight": 0, + "persuasion": 0 + }, + "wdSkillMods": { + "force": 0, + "melee": 0, + "stamina": 0, + "marksmanship": 0, + "mobility": 0, + "stealth": 0, + "crafting": 0, + "observation": 0, + "survival": 0, + "healing": 0, + "insight": 0, + "persuasion": 0 + } + } + }, + { + "name": "Shatter", + "type": "equipment", + "img": "systems/slazero2/assets/SLA_Assets/Drugs/2nd try/shatter.png", + "system": { + "description": "A combat stimulant that shatters the body's pain inhibitors and fatigue limiters simultaneously. Users feel no injury penalties for the duration but sustain damage they don't notice until it wears off.", + "isDrug": true, + "quantity": 1, + "cost": 120, + "weight": 0, + "active": false, + "activeDuration": "1 scene", + "activeLabel": "Shatter Active", + "activeNotes": "+2 Strength effective, ignore injury penalties. Take 1 damage when effect ends.", + "withdrawalActive": false, + "withdrawalDuration": "1 shift", + "withdrawalLabel": "Shatter Crash", + "withdrawalNotes": "-1 die to all rolls, +1 Stress.", + "healthMod": 0, + "resolveMod": -2, + "statPhysicalMod": 2, + "skillAllMod": 0, + "panicReduction": 0, + "injuryPenaltyIgnore": 3, + "stressRecoveryBonus": 0, + "postUseStaminaDamage": 1, + "wdHealthMod": 0, + "wdResolveMod": 0, + "wdStatPhysicalMod": 0, + "wdSkillAllMod": -1, + "wdPanicReduction": 0, + "wdInjuryPenaltyIgnore": 0, + "wdStressRecoveryBonus": 0, + "wdPostUseStaminaDamage": 0, + "skillMods": { + "force": 0, + "melee": 0, + "stamina": 0, + "marksmanship": 0, + "mobility": 0, + "stealth": 0, + "crafting": 0, + "observation": 0, + "survival": 0, + "healing": 0, + "insight": 0, + "persuasion": 0 + }, + "wdSkillMods": { + "force": 0, + "melee": 0, + "stamina": 0, + "marksmanship": 0, + "mobility": 0, + "stealth": 0, + "crafting": 0, + "observation": 0, + "survival": 0, + "healing": 0, + "insight": 0, + "persuasion": 0 + }, + "initiativeMod": 0, + "equipped": false + } + }, + { + "name": "Blaze UV", + "type": "equipment", + "img": "systems/slazero2/assets/SLA_Assets/Drugs/2nd try/Blaze UV.png", + "system": { + "description": "An enhanced variant of UV combat formula. Burns hotter and faster than base UV with correspondingly worse crash. Operatives on Blaze UV are difficult to tell from lightly possessed individuals.", + "isDrug": true, + "quantity": 1, + "cost": 180, + "weight": 0, + "active": false, + "activeDuration": "1 scene", + "activeLabel": "Blaze UV Active", + "activeNotes": "+3 Strength effective, +2 Health max. -4 Resolve. Severe crash.", + "withdrawalActive": false, + "withdrawalDuration": "1 shift", + "withdrawalLabel": "Blaze UV Crash", + "withdrawalNotes": "-2 dice all rolls, +2 Stress, cannot use Ebb.", + "healthMod": 2, + "resolveMod": -4, + "statPhysicalMod": 3, + "skillAllMod": 0, + "panicReduction": 0, + "injuryPenaltyIgnore": 1, + "stressRecoveryBonus": 0, + "postUseStaminaDamage": 2, + "wdHealthMod": 0, + "wdResolveMod": -2, + "wdStatPhysicalMod": 0, + "wdSkillAllMod": -2, + "wdPanicReduction": 0, + "wdInjuryPenaltyIgnore": 0, + "wdStressRecoveryBonus": 0, + "wdPostUseStaminaDamage": 0, + "skillMods": { + "force": 0, + "melee": 0, + "stamina": 0, + "marksmanship": 0, + "mobility": 0, + "stealth": 0, + "crafting": 0, + "observation": 0, + "survival": 0, + "healing": 0, + "insight": 0, + "persuasion": 0 + }, + "wdSkillMods": { + "force": 0, + "melee": 0, + "stamina": 0, + "marksmanship": 0, + "mobility": 0, + "stealth": 0, + "crafting": 0, + "observation": 0, + "survival": 0, + "healing": 0, + "insight": 0, + "persuasion": 0 + }, + "initiativeMod": 0, + "equipped": false + } + }, + { + "name": "Drum", + "type": "equipment", + "img": "systems/slazero2/assets/SLA_Assets/Drugs/2nd try/Drum.png", + "system": { + "description": "A heavy-duty military stimulant dispensed from an industrial container. Bulk combat drug used to dose entire squads. Less refined than other stimulants but extremely effective and cheap per dose.", + "isDrug": true, + "quantity": 1, + "cost": 90, + "weight": 1, + "active": false, + "activeDuration": "1 scene", + "activeLabel": "Drum Active", + "activeNotes": "+1 Strength, +1 Health max. -1 Resolve.", + "withdrawalActive": false, + "withdrawalDuration": "1 shift", + "withdrawalLabel": "Drum Crash", + "withdrawalNotes": "-1 Strength effective, +1 Stress.", + "healthMod": 1, + "resolveMod": -1, + "statPhysicalMod": 1, + "skillAllMod": 0, + "panicReduction": 0, + "injuryPenaltyIgnore": 0, + "stressRecoveryBonus": 0, + "postUseStaminaDamage": 0, + "wdHealthMod": 0, + "wdResolveMod": 0, + "wdStatPhysicalMod": -1, + "wdSkillAllMod": 0, + "wdPanicReduction": 0, + "wdInjuryPenaltyIgnore": 0, + "wdStressRecoveryBonus": 0, + "wdPostUseStaminaDamage": 0, + "skillMods": { + "force": 0, + "melee": 0, + "stamina": 0, + "marksmanship": 0, + "mobility": 0, + "stealth": 0, + "crafting": 0, + "observation": 0, + "survival": 0, + "healing": 0, + "insight": 0, + "persuasion": 0 + }, + "wdSkillMods": { + "force": 0, + "melee": 0, + "stamina": 0, + "marksmanship": 0, + "mobility": 0, + "stealth": 0, + "crafting": 0, + "observation": 0, + "survival": 0, + "healing": 0, + "insight": 0, + "persuasion": 0 + }, + "initiativeMod": 0, + "equipped": false + } + }, + { + "name": "Honesty", + "type": "equipment", + "img": "systems/slazero2/assets/SLA_Assets/Drugs/2nd try/Honesty.png", + "system": { + "description": "A truth serum used by interrogators and intelligence operatives. Targets under Honesty cannot attempt deception or actively lie. They can remain silent. Used with or without consent depending on who's asking.", + "isDrug": true, + "quantity": 1, + "cost": 70, + "weight": 0, + "active": false, + "activeDuration": "1 scene", + "activeLabel": "Honesty Active", + "activeNotes": "Cannot lie. +2 dice to Insight and social observation. -1 Resolve.", + "withdrawalActive": false, + "withdrawalDuration": "", + "withdrawalLabel": "", + "withdrawalNotes": "", + "healthMod": 0, + "resolveMod": -1, + "statPhysicalMod": 0, + "skillAllMod": 0, + "panicReduction": 1, + "injuryPenaltyIgnore": 0, + "stressRecoveryBonus": 0, + "postUseStaminaDamage": 0, + "wdHealthMod": 0, + "wdResolveMod": 0, + "wdStatPhysicalMod": 0, + "wdSkillAllMod": 0, + "wdPanicReduction": 0, + "wdInjuryPenaltyIgnore": 0, + "wdStressRecoveryBonus": 0, + "wdPostUseStaminaDamage": 0, + "skillMods": { + "force": 0, + "melee": 0, + "stamina": 0, + "marksmanship": 0, + "mobility": 0, + "stealth": 0, + "crafting": 0, + "observation": 0, + "survival": 0, + "healing": 0, + "insight": 2, + "persuasion": 0 + }, + "wdSkillMods": { + "force": 0, + "melee": 0, + "stamina": 0, + "marksmanship": 0, + "mobility": 0, + "stealth": 0, + "crafting": 0, + "observation": 0, + "survival": 0, + "healing": 0, + "insight": 0, + "persuasion": 0 + }, + "initiativeMod": 0, + "equipped": false + } + }, + { + "name": "Flip", + "type": "equipment", + "img": "systems/slazero2/assets/SLA_Assets/Drugs/2nd try/Flip.png", + "system": { + "description": "A social lubricant and inhibitor suppressor. Flip makes subjects highly agreeable and talkative. Used recreationally in clubs and professionally in negotiations and after-action debriefs where full truth is desired.", + "isDrug": true, + "quantity": 1, + "cost": 25, + "weight": 0, + "active": false, + "activeDuration": "1 shift", + "activeLabel": "Flip Active", + "activeNotes": "+2 dice Persuasion, +1 dice Insight. -1 Wits effective. Subject will overshare.", + "withdrawalActive": false, + "withdrawalDuration": "", + "withdrawalLabel": "", + "withdrawalNotes": "", + "healthMod": 0, + "resolveMod": 1, + "statPhysicalMod": 0, + "skillAllMod": 0, + "panicReduction": 1, + "injuryPenaltyIgnore": 0, + "stressRecoveryBonus": 1, + "postUseStaminaDamage": 0, + "wdHealthMod": 0, + "wdResolveMod": 0, + "wdStatPhysicalMod": 0, + "wdSkillAllMod": 0, + "wdPanicReduction": 0, + "wdInjuryPenaltyIgnore": 0, + "wdStressRecoveryBonus": 0, + "wdPostUseStaminaDamage": 0, + "skillMods": { + "force": 0, + "melee": 0, + "stamina": 0, + "marksmanship": 0, + "mobility": 0, + "stealth": 0, + "crafting": 0, + "observation": 0, + "survival": 0, + "healing": 0, + "insight": 1, + "persuasion": 2 + }, + "wdSkillMods": { + "force": 0, + "melee": 0, + "stamina": 0, + "marksmanship": 0, + "mobility": 0, + "stealth": 0, + "crafting": 0, + "observation": 0, + "survival": 0, + "healing": 0, + "insight": 0, + "persuasion": 0 + }, + "initiativeMod": 0, + "equipped": false + } + }, + { + "name": "Bozerker", + "type": "equipment", + "img": "systems/slazero2/assets/SLA_Assets/Drugs/2nd try/Bozerker.png", + "system": { + "description": "An experimental overdose-level combat stimulant. Bozerker was never cleared for operational use. Operatives who take it fight at terrifying effectiveness until they collapse. The crash is violent.", + "isDrug": true, + "quantity": 1, + "cost": 220, + "weight": 0, + "active": false, + "activeDuration": "1 scene", + "activeLabel": "BOZERKER ACTIVE", + "activeNotes": "+3 Strength, +3 Health max, ignore ALL injury penalties, +3 panic reduction. Cannot disengage.", + "withdrawalActive": false, + "withdrawalDuration": "1 shift", + "withdrawalLabel": "Bozerker Crash", + "withdrawalNotes": "Incapacitated 1 round. Then -3 all dice, +3 Stress, for 1 scene.", + "healthMod": 3, + "resolveMod": -3, + "statPhysicalMod": 3, + "skillAllMod": 0, + "panicReduction": 3, + "injuryPenaltyIgnore": 5, + "stressRecoveryBonus": 0, + "postUseStaminaDamage": 3, + "wdHealthMod": -2, + "wdResolveMod": -3, + "wdStatPhysicalMod": 0, + "wdSkillAllMod": -3, + "wdPanicReduction": 0, + "wdInjuryPenaltyIgnore": 0, + "wdStressRecoveryBonus": 0, + "wdPostUseStaminaDamage": 0, + "skillMods": { + "force": 0, + "melee": 0, + "stamina": 0, + "marksmanship": 0, + "mobility": 0, + "stealth": 0, + "crafting": 0, + "observation": 0, + "survival": 0, + "healing": 0, + "insight": 0, + "persuasion": 0 + }, + "wdSkillMods": { + "force": 0, + "melee": 0, + "stamina": 0, + "marksmanship": 0, + "mobility": 0, + "stealth": 0, + "crafting": 0, + "observation": 0, + "survival": 0, + "healing": 0, + "insight": 0, + "persuasion": 0 + }, + "initiativeMod": 0, + "equipped": false + } + }, + { + "name": "Push", + "type": "equipment", + "img": "systems/slazero2/assets/SLA_Assets/Drugs/2nd try/Push.png", + "system": { + "description": "A medical auto-injector that restores consciousness and stabilises critical injuries. Push is used by combat medics to bring downed operatives back into the fight. It does not heal — it delays.", + "isDrug": true, + "quantity": 1, + "cost": 140, + "weight": 0, + "active": false, + "activeDuration": "1 scene", + "activeLabel": "Push Active", + "activeNotes": "+2 Health max, ignore Broken condition, +1 dice Stamina. Effects collapse after scene.", + "withdrawalActive": false, + "withdrawalDuration": "", + "withdrawalLabel": "", + "withdrawalNotes": "", + "healthMod": 2, + "resolveMod": 0, + "statPhysicalMod": 0, + "skillAllMod": 0, + "panicReduction": 0, + "injuryPenaltyIgnore": 2, + "stressRecoveryBonus": 0, + "postUseStaminaDamage": 0, + "wdHealthMod": 0, + "wdResolveMod": 0, + "wdStatPhysicalMod": 0, + "wdSkillAllMod": 0, + "wdPanicReduction": 0, + "wdInjuryPenaltyIgnore": 0, + "wdStressRecoveryBonus": 0, + "wdPostUseStaminaDamage": 0, + "skillMods": { + "force": 0, + "melee": 0, + "stamina": 1, + "marksmanship": 0, + "mobility": 0, + "stealth": 0, + "crafting": 0, + "observation": 0, + "survival": 0, + "healing": 0, + "insight": 0, + "persuasion": 0 + }, + "wdSkillMods": { + "force": 0, + "melee": 0, + "stamina": 0, + "marksmanship": 0, + "mobility": 0, + "stealth": 0, + "crafting": 0, + "observation": 0, + "survival": 0, + "healing": 0, + "insight": 0, + "persuasion": 0 + }, + "initiativeMod": 0, + "equipped": false + } + }, + { + "name": "Bubbles", + "type": "equipment", + "img": "systems/slazero2/assets/SLA_Assets/Drugs/2nd try/Bubbles.png", + "system": { + "description": "A recreational effervescent tablet. Mild euphoric and stress reducer. Widely available and technically legal in most Mort districts. Commonly taken before social events, interrogations, or anything that requires appearing calm.", + "isDrug": true, + "quantity": 1, + "cost": 18, + "weight": 0, + "active": false, + "activeDuration": "1 shift", + "activeLabel": "Bubbles Active", + "activeNotes": "+1 Resolve, +1 panic reduction, +1 Stress recovery bonus.", + "withdrawalActive": false, + "withdrawalDuration": "", + "withdrawalLabel": "", + "withdrawalNotes": "", + "healthMod": 0, + "resolveMod": 1, + "statPhysicalMod": 0, + "skillAllMod": 0, + "panicReduction": 1, + "injuryPenaltyIgnore": 0, + "stressRecoveryBonus": 1, + "postUseStaminaDamage": 0, + "wdHealthMod": 0, + "wdResolveMod": 0, + "wdStatPhysicalMod": 0, + "wdSkillAllMod": 0, + "wdPanicReduction": 0, + "wdInjuryPenaltyIgnore": 0, + "wdStressRecoveryBonus": 0, + "wdPostUseStaminaDamage": 0, + "skillMods": { + "force": 0, + "melee": 0, + "stamina": 0, + "marksmanship": 0, + "mobility": 0, + "stealth": 0, + "crafting": 0, + "observation": 0, + "survival": 0, + "healing": 0, + "insight": 0, + "persuasion": 0 + }, + "wdSkillMods": { + "force": 0, + "melee": 0, + "stamina": 0, + "marksmanship": 0, + "mobility": 0, + "stealth": 0, + "crafting": 0, + "observation": 0, + "survival": 0, + "healing": 0, + "insight": 0, + "persuasion": 0 + }, + "initiativeMod": 0, + "equipped": false + } + }, + { + "name": "Slosh", + "type": "equipment", + "img": "systems/slazero2/assets/SLA_Assets/Drugs/2nd try/Slosh.png", + "system": { + "description": "A cheap Mort street alcohol in a hip flask. Poor quality, universally available, and culturally ubiquitous. Reduces stress but impairs everything else. The unofficial recreational drug of SLA operatives on down time.", + "isDrug": true, + "quantity": 1, + "cost": 15, + "weight": 0, + "active": false, + "activeDuration": "1 shift", + "activeLabel": "Slosh Active", + "activeNotes": "+2 Resolve, -1 dice all action rolls. Cheap stress relief.", + "withdrawalActive": false, + "withdrawalDuration": "", + "withdrawalLabel": "", + "withdrawalNotes": "", + "healthMod": 0, + "resolveMod": 2, + "statPhysicalMod": 0, + "skillAllMod": -1, + "panicReduction": 2, + "injuryPenaltyIgnore": 0, + "stressRecoveryBonus": 2, + "postUseStaminaDamage": 0, + "wdHealthMod": 0, + "wdResolveMod": 0, + "wdStatPhysicalMod": 0, + "wdSkillAllMod": 0, + "wdPanicReduction": 0, + "wdInjuryPenaltyIgnore": 0, + "wdStressRecoveryBonus": 0, + "wdPostUseStaminaDamage": 0, + "skillMods": { + "force": 0, + "melee": 0, + "stamina": 0, + "marksmanship": 0, + "mobility": 0, + "stealth": 0, + "crafting": 0, + "observation": 0, + "survival": 0, + "healing": 0, + "insight": 0, + "persuasion": 0 + }, + "wdSkillMods": { + "force": 0, + "melee": 0, + "stamina": 0, + "marksmanship": 0, + "mobility": 0, + "stealth": 0, + "crafting": 0, + "observation": 0, + "survival": 0, + "healing": 0, + "insight": 0, + "persuasion": 0 + }, + "initiativeMod": 0, + "equipped": false + } + }, + { + "name": "KickStart", + "type": "equipment", + "img": "systems/slazero2/assets/SLA_Assets/Drugs/Generated/kickstart.svg", + "system": { + "description": "Karma's iconic field-medical drug. Powerful coagulants stop internal bleeding on contact; secondary compounds accelerate tissue regrowth and knit minor wounds at remarkable speed. Applied as an aerosol spray directly to open wounds or injected at a wound site. Designed to be addictive — Karma ensures repeat custom.", + "isDrug": true, + "quantity": 1, + "cost": 25, + "active": false, + "activeDuration": "Immediate (coagulants active 1 scene)", + "activeLabel": "", + "activeNotes": "Stops bleeding; restores 2 HP from trauma. Reduces one injury penalty level.", + "withdrawalActive": false, + "withdrawalDuration": "1 shift", + "withdrawalLabel": "", + "withdrawalNotes": "Mild physical addiction — roll Stamina after 5 doses. Cravings cause -1 Stamina on failure.", + "healthMod": 0, + "resolveMod": 0, + "statPhysicalMod": 0, + "skillAllMod": 0, + "panicReduction": 0, + "injuryPenaltyIgnore": 1, + "stressRecoveryBonus": 0, + "postUseStaminaDamage": 0, + "wdHealthMod": 0, + "wdResolveMod": 0, + "wdStatPhysicalMod": -1, + "wdSkillAllMod": 0, + "wdPanicReduction": 0, + "wdInjuryPenaltyIgnore": 0, + "wdStressRecoveryBonus": 0, + "wdPostUseStaminaDamage": 0, + "skillMods": { + "force": 0, + "melee": 0, + "stamina": 0, + "marksmanship": 0, + "mobility": 0, + "stealth": 0, + "crafting": 0, + "observation": 0, + "survival": 0, + "healing": 1, + "insight": 0, + "persuasion": 0 + }, + "wdSkillMods": { + "force": 0, + "melee": 0, + "stamina": -1, + "marksmanship": 0, + "mobility": 0, + "stealth": 0, + "crafting": 0, + "observation": 0, + "survival": 0, + "healing": 0, + "insight": 0, + "persuasion": 0 + }, + "healthHeal": 2 + } + }, + { + "name": "KickStart Plus", + "type": "equipment", + "img": "systems/slazero2/assets/SLA_Assets/Drugs/Generated/kickstart-plus.svg", + "system": { + "description": "Enhanced KickStart with higher-concentration coagulants, additional growth-factor compounds, and a secondary stimulant component that forces the body to fight harder against trauma. The stimulant aspect causes mild mental agitation. Injected; popular with Frothers and veterans who need faster recovery under fire. More addictive than the base formula — Karma's intentional design.", + "isDrug": true, + "quantity": 1, + "cost": 50, + "active": false, + "activeDuration": "1 scene", + "activeLabel": "", + "activeNotes": "+3 HP, +1 Stamina skill, ignores 1 injury penalty. -1 Resolve from stimulant agitation.", + "withdrawalActive": false, + "withdrawalDuration": "1 shift", + "withdrawalLabel": "", + "withdrawalNotes": "Moderate addiction — roll Stamina after 3 doses. Withdrawal causes fatigue and -1 to physical stat and Mobility.", + "healthMod": 0, + "resolveMod": -1, + "statPhysicalMod": 0, + "skillAllMod": 0, + "panicReduction": 0, + "injuryPenaltyIgnore": 1, + "stressRecoveryBonus": 0, + "postUseStaminaDamage": 0, + "wdHealthMod": 0, + "wdResolveMod": -1, + "wdStatPhysicalMod": -1, + "wdSkillAllMod": 0, + "wdPanicReduction": 0, + "wdInjuryPenaltyIgnore": 0, + "wdStressRecoveryBonus": 0, + "wdPostUseStaminaDamage": 0, + "skillMods": { + "force": 0, + "melee": 0, + "stamina": 1, + "marksmanship": 0, + "mobility": 0, + "stealth": 0, + "crafting": 0, + "observation": 0, + "survival": 0, + "healing": 1, + "insight": 0, + "persuasion": 0 + }, + "wdSkillMods": { + "force": 0, + "melee": 0, + "stamina": -1, + "marksmanship": 0, + "mobility": -1, + "stealth": 0, + "crafting": 0, + "observation": 0, + "survival": 0, + "healing": 0, + "insight": 0, + "persuasion": 0 + }, + "healthHeal": 3 + } + }, + { + "name": "KickStart Solo", + "type": "equipment", + "img": "systems/slazero2/assets/SLA_Assets/Drugs/Generated/kickstart-solo.svg", + "system": { + "description": "Pre-loaded single-use auto-injector designed for solo Operatives with no medic support. One-handed fast self-administration — no Healing skill required. Stronger pain-suppression component than the base formula keeps the Operative functional after self-injection under fire. Standard emergency field kit for lone SLA Operatives.", + "isDrug": true, + "quantity": 1, + "cost": 40, + "active": false, + "activeDuration": "Immediate (pain suppression lasts 1 scene)", + "activeLabel": "", + "activeNotes": "Self-administer as a fast action — no Healing roll. +2 HP, ignores 2 injury penalty levels.", + "withdrawalActive": false, + "withdrawalDuration": "1 shift", + "withdrawalLabel": "", + "withdrawalNotes": "Same physiological addiction as KickStart base. Roll Stamina after 5 doses.", + "healthMod": 0, + "resolveMod": 0, + "statPhysicalMod": 0, + "skillAllMod": 0, + "panicReduction": 0, + "injuryPenaltyIgnore": 2, + "stressRecoveryBonus": 0, + "postUseStaminaDamage": 0, + "wdHealthMod": 0, + "wdResolveMod": 0, + "wdStatPhysicalMod": -1, + "wdSkillAllMod": 0, + "wdPanicReduction": 0, + "wdInjuryPenaltyIgnore": 0, + "wdStressRecoveryBonus": 0, + "wdPostUseStaminaDamage": 0, + "skillMods": { + "force": 0, + "melee": 0, + "stamina": 0, + "marksmanship": 0, + "mobility": 0, + "stealth": 0, + "crafting": 0, + "observation": 0, + "survival": 0, + "healing": 0, + "insight": 0, + "persuasion": 0 + }, + "wdSkillMods": { + "force": 0, + "melee": 0, + "stamina": -1, + "marksmanship": 0, + "mobility": 0, + "stealth": 0, + "crafting": 0, + "observation": 0, + "survival": 0, + "healing": 0, + "insight": 0, + "persuasion": 0 + }, + "healthHeal": 2 + } + } +] diff --git a/packs/ebb-formulae.json b/packs/ebb-formulae.json new file mode 100644 index 0000000..f9f29c1 --- /dev/null +++ b/packs/ebb-formulae.json @@ -0,0 +1,452 @@ +[ + { + "name": "Empathic Pulse", + "type": "ebb", + "img": "systems/slazero2/assets/SLA_Assets/Ebb/Awareness.png", + "system": { + "description": "The Ebon reaches outward with focused ebb energy, sensing the emotional landscape of nearby minds. Used by investigators to read a room before entering and by field operatives to detect concealed hostiles.", + "discipline": "awareness", + "fluxCost": 1, + "successes": 1, + "effect": "Sense the dominant emotion of every living being within Short range. Gain +1 die to Insight rolls against sensed targets this stretch.", + "catastrophe": "Overwhelmed by alien emotions; +1 Stress and -1 die to all Empathy rolls for 1 round.", + "duration": "Stretch", + "range": "Short" + } + }, + { + "name": "Precognition", + "type": "ebb", + "img": "systems/slazero2/assets/SLA_Assets/Ebb/Awareness.png", + "system": { + "description": "The Ebon projects their awareness a handful of seconds into the future, glimpsing outcomes before they crystallise. Even a brief forewarning can mean the difference between survival and oblivion on the streets of Mort.", + "discipline": "awareness", + "fluxCost": 2, + "successes": 2, + "effect": "Ask the GM one yes/no question about what will happen in the next few minutes. The answer is truthful but may be incomplete.", + "catastrophe": "Fractured timeline vision; Stunned for 1 round (no actions), +1 Stress.", + "duration": "Instant", + "range": "Self" + } + }, + { + "name": "Danger Sense", + "type": "ebb", + "img": "systems/slazero2/assets/SLA_Assets/Ebb/Awareness.png", + "system": { + "description": "The Ebon maintains a perpetual low-level awareness field, turning the ebb into an early-warning system. Ambushes become nearly impossible and hostiles register as a cold prickle at the base of the skull before they act.", + "discipline": "awareness", + "fluxCost": 2, + "successes": 1, + "effect": "Cannot be surprised for 1 shift. Gain +2 dice to Initiative rolls. Sense aggression directed at you before it manifests.", + "catastrophe": "Hyper-vigilance overload; +2 Stress, must flee or attack the nearest perceived threat on next action.", + "duration": "Shift", + "range": "Self" + } + }, + { + "name": "Ebb Bolt", + "type": "ebb", + "img": "systems/slazero2/assets/SLA_Assets/Ebb/Blast.png", + "system": { + "description": "The most fundamental offensive formula \u2014 raw ebb energy concentrated into a kinetic projectile. Every trained combat Ebon learns this first. Fast, reliable, and visible as a crackling blue-white streak.", + "discipline": "blast", + "fluxCost": 1, + "successes": 1, + "effect": "Strike one target for damage equal to successes. Armor applies normally.", + "catastrophe": "Backlash; Caster takes 1 damage and +1 Stress as ebb energy reverses.", + "duration": "Instant", + "range": "Short" + } + }, + { + "name": "Hellfire", + "type": "ebb", + "img": "systems/slazero2/assets/SLA_Assets/Ebb/Blast.png", + "system": { + "description": "A concentrated ebb detonation that releases stored energy in a single catastrophic burst. The air turns orange-white and the impact leaves scorched craters. SLA is cautious about licensing Ebons who favour this formula \u2014 it is extremely effective and even more visible.", + "discipline": "blast", + "fluxCost": 2, + "successes": 2, + "effect": "Blast one target for successes + 2 damage. On 3+ successes, the blast ignites flammable materials and deals 1 splash damage to all Engaged targets.", + "catastrophe": "Misfire; Detonation occurs at caster's feet. Caster takes 2 damage, all Engaged characters take 1 damage.", + "duration": "Instant", + "range": "Short" + } + }, + { + "name": "Ebb Storm", + "type": "ebb", + "img": "systems/slazero2/assets/SLA_Assets/Ebb/Blast.png", + "system": { + "description": "The most destructive Blast formula in common circulation, Ebb Storm saturates an area with rapidly cycling ebb frequencies until everything inside is shredded. Restricted to Department of Ebb-use oversight. Usage reports are mandatory.", + "discipline": "blast", + "fluxCost": 3, + "successes": 2, + "effect": "Ebb energy detonates at Short range from the caster. All targets in the burst radius take damage equal to successes (reduce by 1 per additional target beyond the first, minimum 1).", + "catastrophe": "Uncontrolled surge; All characters within Short range of caster \u2014 including allies \u2014 take 2 damage.", + "duration": "Instant", + "range": "Short" + } + }, + { + "name": "Mind-Link", + "type": "ebb", + "img": "systems/slazero2/assets/SLA_Assets/Ebb/Communicate.png", + "system": { + "description": "A direct, unmediated channel between two minds. Words, images, and intent pass faster than speech. Standard kit for any Ebon working alongside non-ebb teammates who lack radio discipline.", + "discipline": "communicate", + "fluxCost": 1, + "successes": 1, + "effect": "Establish silent two-way mental communication with one willing target within Short range for 1 stretch. Undetectable by mundane surveillance.", + "catastrophe": "Static bleed; Both caster and target gain +1 Stress from the mental noise feedback.", + "duration": "Stretch", + "range": "Short" + } + }, + { + "name": "Broadcast", + "type": "ebb", + "img": "systems/slazero2/assets/SLA_Assets/Ebb/Communicate.png", + "system": { + "description": "A one-way mental shout projected into every nearby mind simultaneously. Useful for mass commands, warnings, or demoralisation. Brain Wasters favour this formula for spreading raw terror.", + "discipline": "communicate", + "fluxCost": 2, + "successes": 1, + "effect": "Project one simple message, image, or emotion to all living beings within Long range. Cannot be blocked. Message is brief \u2014 one sentence or strong impression.", + "catastrophe": "Neural bleed; All targets, including allies, experience disorienting psychic noise. All within range lose their next Fast action.", + "duration": "Instant", + "range": "Long" + } + }, + { + "name": "Compel", + "type": "ebb", + "img": "systems/slazero2/assets/SLA_Assets/Ebb/Communicate.png", + "system": { + "description": "The Ebon reaches into a target's decision-making architecture and plants a simple directive that feels entirely like the target's own idea. Extremely illegal without SLA authorisation. Used extensively by the Department of Investigation.", + "discipline": "communicate", + "fluxCost": 2, + "successes": 3, + "effect": "Force one target to perform a simple action (flee, drop weapon, sit down). Target resists with Empathy roll (3 successes to resist). On success, they comply believing it was their own choice.", + "catastrophe": "Psychic feedback; Target becomes hostile to the caster. Both gain +1 Stress.", + "duration": "Instant", + "range": "Engaged" + } + }, + { + "name": "Surge", + "type": "ebb", + "img": "systems/slazero2/assets/SLA_Assets/Ebb/Enhancement.png", + "system": { + "description": "A rapid-onset ebb boost that floods the target's neural pathways with enhanced signal clarity. Physical or mental performance spikes sharply for a brief window \u2014 long enough to matter.", + "discipline": "enhance", + "fluxCost": 1, + "successes": 1, + "effect": "Add +2 dice to all rolls using one chosen attribute (Strength, Agility, Wits, or Empathy) for 1 round.", + "catastrophe": "Overclocked; When the effect ends, gain +1 Stress and suffer -1 die to all rolls for 1 round as the crash hits.", + "duration": "Round", + "range": "Self" + } + }, + { + "name": "Iron Flesh", + "type": "ebb", + "img": "systems/slazero2/assets/SLA_Assets/Ebb/Enhancement.png", + "system": { + "description": "The Ebon's skin hardens as ebb energy reinforces cellular structure \u2014 faint crackling can be heard if you listen close. The effect is temporary but renders them significantly harder to injure.", + "discipline": "enhance", + "fluxCost": 2, + "successes": 2, + "effect": "Gain 2 automatic armor (stacks with worn armor) for 1 stretch. This ebb-armor is not reduced by AP.", + "catastrophe": "Brittle shell; The hardening shatters immediately and the caster takes 1 damage from the rebound feedback.", + "duration": "Stretch", + "range": "Self" + } + }, + { + "name": "Heightened State", + "type": "ebb", + "img": "systems/slazero2/assets/SLA_Assets/Ebb/Enhancement.png", + "system": { + "description": "The pinnacle of Enhance: the Ebon exists at the absolute limit of their potential for a brief, perfect moment. Operatives who have experienced this describe it as the most alive they have ever felt \u2014 immediately before the crash.", + "discipline": "enhance", + "fluxCost": 3, + "successes": 2, + "effect": "Gain +1 die to all rolls, reduce all incoming damage by 1 (minimum 1), and cannot be Broken for the duration.", + "catastrophe": "Crash; When the effect expires or fails, immediately become Broken. +2 Stress.", + "duration": "Stretch", + "range": "Self" + } + }, + { + "name": "Mend", + "type": "ebb", + "img": "systems/slazero2/assets/SLA_Assets/Ebb/Heal.png", + "system": { + "description": "The Heal discipline's most basic application \u2014 the Ebon channels ebb energy directly into biological tissue, accelerating natural repair processes. Routine field medicine for any squad with an Ebon operative.", + "discipline": "heal", + "fluxCost": 1, + "successes": 1, + "effect": "Restore Health equal to successes (max 2) to self or a touched target. Cannot exceed maximum Health.", + "catastrophe": "Drain; Caster takes 1 damage as bio-energy flows the wrong direction through the link.", + "duration": "Instant", + "range": "Engaged" + } + }, + { + "name": "Restore", + "type": "ebb", + "img": "systems/slazero2/assets/SLA_Assets/Ebb/Heal.png", + "system": { + "description": "A significant investment of flux that triggers deep biological regeneration. Bone knits, blood stops. On exceptional rolls, even light critical injuries can be suppressed \u2014 though not truly healed.", + "discipline": "heal", + "fluxCost": 2, + "successes": 2, + "effect": "Restore Health equal to successes + 1. On 3+ successes, also clear one non-lethal Critical Injury.", + "catastrophe": "Empathic wound; Caster absorbs the target's injuries. Caster takes damage equal to the target's critical count (1-3).", + "duration": "Instant", + "range": "Engaged" + } + }, + { + "name": "Calm", + "type": "ebb", + "img": "systems/slazero2/assets/SLA_Assets/Ebb/Heal.png", + "system": { + "description": "The Ebon reaches into the ebb frequencies associated with panic and distress, smoothing them into something bearable. Standard issue for any operative working in trauma zones or prolonged high-stress environments.", + "discipline": "heal", + "fluxCost": 1, + "successes": 1, + "effect": "Reduce one target's Stress by 1. If they are Broken from Stress, they may immediately attempt a recovery roll.", + "catastrophe": "Shared anguish; Caster absorbs the emotional load. Caster gains +1 Stress.", + "duration": "Instant", + "range": "Engaged" + } + }, + { + "name": "Ebb Shield", + "type": "ebb", + "img": "systems/slazero2/assets/SLA_Assets/Ebb/Protect.png", + "system": { + "description": "A reactive personal barrier of condensed ebb energy that interposes itself between the caster and incoming harm. The shimmer is visible \u2014 blue-white, flickering at impact. It does not last long but can turn the critical strike.", + "discipline": "protect", + "fluxCost": 1, + "successes": 1, + "effect": "Roll 2d6 against the next incoming attack this round; each 6 reduces damage by 1. The shield can absorb up to 3 damage total before collapsing.", + "catastrophe": "Barrier collapse; The shield implodes. Caster takes 1 damage and +1 Stress.", + "duration": "Round", + "range": "Self" + } + }, + { + "name": "Ward", + "type": "ebb", + "img": "systems/slazero2/assets/SLA_Assets/Ebb/Protect.png", + "system": { + "description": "The Ebon extends a protective field to cover an ally, wrapping them in deflection energy. The ward holds until it absorbs a direct ebb attack or expires naturally.", + "discipline": "protect", + "fluxCost": 2, + "successes": 2, + "effect": "One target within Short range gains 1 automatic armor for 1 stretch. The ward also absorbs one incoming Ebb formula completely (then dissipates).", + "catastrophe": "Overextension; Both caster and target are momentarily unprotected. All attacks against either gain +1 die this round.", + "duration": "Stretch", + "range": "Short" + } + }, + { + "name": "Phase Shell", + "type": "ebb", + "img": "systems/slazero2/assets/SLA_Assets/Ebb/Protect.png", + "system": { + "description": "The Ebon shifts their personal ebb signature out of phase with normal matter, causing incoming projectiles and blows to partially pass through. The most extreme defensive formula available \u2014 and the most taxing.", + "discipline": "protect", + "fluxCost": 3, + "successes": 3, + "effect": "All physical damage is reduced by 3 this round. Ebb attacks must overcome an opposed Ebb roll to penetrate. Movement is halved.", + "catastrophe": "Phase lock; Frozen in phase-space for 1 round \u2014 cannot act, but cannot be harmed. Extremely disorienting.", + "duration": "Round", + "range": "Self" + } + }, + { + "name": "Blink", + "type": "ebb", + "img": "systems/slazero2/assets/SLA_Assets/Ebb/Reality Fold.png", + "system": { + "description": "Short-range teleportation via a momentary fold in local space-time. The Ebon vanishes with a crack of displaced air and reappears somewhere within sight. Extremely useful in enclosed firefights.", + "discipline": "realityFold", + "fluxCost": 2, + "successes": 2, + "effect": "Instantly teleport to any visible point within Short range. Cannot pass through solid barriers.", + "catastrophe": "Partial fold; Materialise partially inside an obstacle. Take 2 damage and are knocked prone.", + "duration": "Instant", + "range": "Self" + } + }, + { + "name": "Fold Space", + "type": "ebb", + "img": "systems/slazero2/assets/SLA_Assets/Ebb/Reality Fold.png", + "system": { + "description": "Two points in space become adjacent for a brief window. Tactically absurd. The GM decides who can react fast enough when a wall suddenly has a window to a point fifty metres away.", + "discipline": "realityFold", + "fluxCost": 3, + "successes": 3, + "effect": "Two chosen points within range become adjacent for 1 round. Line of sight and attacks between them occur as if at Engaged range.", + "catastrophe": "Space collapse; Both fold points detonate. Everyone Engaged with either point takes 2 damage.", + "duration": "Round", + "range": "Medium" + } + }, + { + "name": "Phase Through", + "type": "ebb", + "img": "systems/slazero2/assets/SLA_Assets/Ebb/Reality Fold.png", + "system": { + "description": "The Ebon shifts their atomic structure briefly out of alignment with normal matter, allowing them to walk through a single solid non-living obstacle. The sensation is described as cold static filling every cell.", + "discipline": "realityFold", + "fluxCost": 3, + "successes": 2, + "effect": "Pass through one non-living solid object (wall, door, hull). Takes one action. Cannot phase through living tissue or active ebb fields.", + "catastrophe": "Partial phase; Stuck inside the material. Cannot act, take 1 damage per round until freed by allies or own effort.", + "duration": "Instant", + "range": "Self" + } + }, + { + "name": "Ebb Sight", + "type": "ebb", + "img": "systems/slazero2/assets/SLA_Assets/Ebb/Senses.png", + "system": { + "description": "The Ebon perceives the ebb spectrum \u2014 active formulae glow like flares, dormant Ebons are visible as faint heat shimmer, and residual ebb discharge lingers like burn marks on reality.", + "discipline": "senses", + "fluxCost": 1, + "successes": 1, + "effect": "Perceive ebb energy, active formulae, and ebb users within Long range as visible auras. Gain +2 dice to detect ebb-related activity. See clearly in darkness.", + "catastrophe": "Ebb blindness; Normal vision disrupted by ebb noise. -2 dice to Observation for 1 round.", + "duration": "Stretch", + "range": "Long" + } + }, + { + "name": "Mind's Eye", + "type": "ebb", + "img": "systems/slazero2/assets/SLA_Assets/Ebb/Senses.png", + "system": { + "description": "All five senses sharpen to preternatural levels simultaneously. The Ebon can read a serial number at Long range, hear whispers through walls, and track by scent. Over-reliance leads to the sensation of perpetual information overload.", + "discipline": "senses", + "fluxCost": 1, + "successes": 1, + "effect": "Gain +3 dice to all Observation rolls. Cannot be ambushed. Detect concealed objects and persons within Short range automatically.", + "catastrophe": "Sensory overload; Stunned for 1 round (no actions), +1 Stress.", + "duration": "Stretch", + "range": "Self" + } + }, + { + "name": "Remote View", + "type": "ebb", + "img": "systems/slazero2/assets/SLA_Assets/Ebb/Senses.png", + "system": { + "description": "The Ebon projects their senses to a remote location, seeing and hearing as though present. The body remains inert and vulnerable while the mind wanders. Standard operational use requires a minder.", + "discipline": "senses", + "fluxCost": 2, + "successes": 2, + "effect": "Project senses to any location within Long range that you have visited or can clearly visualise. See and hear but cannot interact.", + "catastrophe": "Lost projection; Consciousness becomes stuck. Cannot act for 1 round \u2014 must be physically shaken free. +1 Stress.", + "duration": "Stretch", + "range": "Long" + } + }, + { + "name": "Push", + "type": "ebb", + "img": "systems/slazero2/assets/SLA_Assets/Ebb/Telekinesis.png", + "system": { + "description": "The most common Telekinesis formula and a staple of Ebon combat training. A directed burst of kinetic force that can clear corridors, send opponents sprawling, or shove obstacles.", + "discipline": "telekinesis", + "fluxCost": 1, + "successes": 1, + "effect": "Hurl one target or object with mental force, moving them up to Short range. Living targets take 1 damage and must pass Strength (2 successes) or fall prone.", + "catastrophe": "Misaimed; The force shoves the caster instead. Caster is knocked back and falls prone.", + "duration": "Instant", + "range": "Short" + } + }, + { + "name": "Grip", + "type": "ebb", + "img": "systems/slazero2/assets/SLA_Assets/Ebb/Telekinesis.png", + "system": { + "description": "An invisible hand seizes the target and holds them immobile. The hold is not gentle \u2014 internal pressure builds noticeably. Prolonged use causes real physical damage.", + "discipline": "telekinesis", + "fluxCost": 2, + "successes": 2, + "effect": "Hold one target immobile (they can still act but cannot move). Deal damage equal to successes at the start of each round the grip is maintained. Requires a new roll each round.", + "catastrophe": "Mental strain; Grip snaps. Caster takes 1 damage and +1 Stress.", + "duration": "Round", + "range": "Short" + } + }, + { + "name": "Levitate", + "type": "ebb", + "img": "systems/slazero2/assets/SLA_Assets/Ebb/Telekinesis.png", + "system": { + "description": "The Ebon defies gravity with casual elegance. Movement is in any direction \u2014 vertical, horizontal, diagonal \u2014 at normal walking speed. A discreet entrance through a third-storey window is standard procedure.", + "discipline": "telekinesis", + "fluxCost": 2, + "successes": 1, + "effect": "Float at any height, moving in any direction at normal speed. Immune to fall damage while active. On 3+ successes, can carry one additional person.", + "catastrophe": "Sudden drop; Fall from current height and take normal fall damage.", + "duration": "Stretch", + "range": "Self" + } + }, + { + "name": "Heat Touch", + "type": "ebb", + "img": "systems/slazero2/assets/SLA_Assets/Ebb/Thermal Blue.png", + "system": { + "description": "Superheating the immediate cellular layer through Thermal manipulation is a brutal and intimate formula. Equipment melts, flesh chars. Favoured by Ebons who prefer to work at close quarters.", + "discipline": "thermal", + "fluxCost": 1, + "successes": 1, + "effect": "Deal damage equal to successes + 1 to a touched target. On 3+ successes, their clothing or equipment begins to burn (1 damage per round until extinguished).", + "catastrophe": "Backburn; Heat reverses. Caster takes 1 damage as the formula collapses inward.", + "duration": "Instant", + "range": "Engaged" + } + }, + { + "name": "Flame Burst", + "type": "ebb", + "img": "systems/slazero2/assets/SLA_Assets/Ebb/Thermal Blue.png", + "system": { + "description": "A ranged thermal projection \u2014 concentrated heat launched as a coherent bolt. The Ebon's eyes typically take on a faint orange glow while channelling. The impact smells of burning ozone and scorched metal.", + "discipline": "thermal", + "fluxCost": 2, + "successes": 1, + "effect": "Deal damage equal to successes + 2. On 3+ successes, the target catches fire (1 damage per round until extinguished by action or water).", + "catastrophe": "Flash; Caster and all Engaged targets are blinded for 1 round.", + "duration": "Instant", + "range": "Short" + } + }, + { + "name": "Freeze", + "type": "ebb", + "img": "systems/slazero2/assets/SLA_Assets/Ebb/Thermal Blue.png", + "system": { + "description": "Thermal extraction removes heat from the target rapidly. Ice crystals form in joint fluid, synaptic firing slows, and movement becomes laboured agony. Objects frozen in this manner become dangerously brittle.", + "discipline": "thermal", + "fluxCost": 2, + "successes": 2, + "effect": "Target takes 1 damage and suffers -2 dice to all Agility rolls this round. Objects may shatter if frozen.", + "catastrophe": "Thermal shock; Temperature flux knocks caster prone. +1 Stress.", + "duration": "Round", + "range": "Short" + } + } +] \ No newline at end of file diff --git a/packs/equipment.json b/packs/equipment.json new file mode 100644 index 0000000..3f3298e --- /dev/null +++ b/packs/equipment.json @@ -0,0 +1,728 @@ +[ + { + "name": "BOOPA Medical Kit", + "type": "equipment", + "img": "systems/slazero2/assets/SLA_Assets/Gear/Boopa Medi Kit Advanced.png", + "system": { + "description": "Standard SLA-issue medical kit. Contains wound dressings, haemostatic foam, trauma shears, and two doses of field analgesic. Allows Healing rolls in the field. Without a kit, field healing is at -2 dice.", + "isDrug": false, + "quantity": 1, + "cost": 80, + "weight": 1, + "active": false, + "activeDuration": "", + "activeLabel": "", + "activeNotes": "", + "withdrawalActive": false, + "withdrawalDuration": "", + "withdrawalLabel": "", + "withdrawalNotes": "", + "healthMod": 0, + "resolveMod": 0, + "statPhysicalMod": 0, + "skillAllMod": 0, + "panicReduction": 0, + "injuryPenaltyIgnore": 0, + "stressRecoveryBonus": 0, + "postUseStaminaDamage": 0, + "wdHealthMod": 0, + "wdResolveMod": 0, + "wdStatPhysicalMod": 0, + "wdSkillAllMod": 0, + "wdPanicReduction": 0, + "wdInjuryPenaltyIgnore": 0, + "wdStressRecoveryBonus": 0, + "wdPostUseStaminaDamage": 0, + "skillMods": { + "force": 0, + "melee": 0, + "stamina": 0, + "marksmanship": 0, + "mobility": 0, + "stealth": 0, + "crafting": 0, + "observation": 0, + "survival": 0, + "healing": 2, + "insight": 0, + "persuasion": 0 + }, + "wdSkillMods": { + "force": 0, + "melee": 0, + "stamina": 0, + "marksmanship": 0, + "mobility": 0, + "stealth": 0, + "crafting": 0, + "observation": 0, + "survival": 0, + "healing": 0, + "insight": 0, + "persuasion": 0 + }, + "initiativeMod": 0, + "equipped": false + } + }, + { + "name": "Stim Patch", + "type": "equipment", + "img": "systems/slazero2/assets/SLA_Assets/Gear/Klippo Spray Hypo.png", + "system": { + "description": "A transdermal stimulant patch. Applied to skin and activated by pressing the centre. Delivers a controlled dose of combat stimulant over 10 minutes. Slower than injection but discreet and hands-free.", + "isDrug": true, + "quantity": 1, + "cost": 35, + "weight": 0, + "active": false, + "activeDuration": "1 scene", + "activeLabel": "Stim Patch Active", + "activeNotes": "+1 Resolve, +1 Strength effective. Slower onset than injected drugs.", + "withdrawalActive": false, + "withdrawalDuration": "", + "withdrawalLabel": "", + "withdrawalNotes": "", + "healthMod": 0, + "resolveMod": 1, + "statPhysicalMod": 1, + "skillAllMod": 0, + "panicReduction": 0, + "injuryPenaltyIgnore": 0, + "stressRecoveryBonus": 0, + "postUseStaminaDamage": 0, + "wdHealthMod": 0, + "wdResolveMod": 0, + "wdStatPhysicalMod": 0, + "wdSkillAllMod": 0, + "wdPanicReduction": 0, + "wdInjuryPenaltyIgnore": 0, + "wdStressRecoveryBonus": 0, + "wdPostUseStaminaDamage": 0, + "skillMods": { + "force": 0, + "melee": 0, + "stamina": 0, + "marksmanship": 0, + "mobility": 0, + "stealth": 0, + "crafting": 0, + "observation": 0, + "survival": 0, + "healing": 0, + "insight": 0, + "persuasion": 0 + }, + "wdSkillMods": { + "force": 0, + "melee": 0, + "stamina": 0, + "marksmanship": 0, + "mobility": 0, + "stealth": 0, + "crafting": 0, + "observation": 0, + "survival": 0, + "healing": 0, + "insight": 0, + "persuasion": 0 + }, + "initiativeMod": 0, + "equipped": false + } + }, + { + "name": "Bandage Pack", + "type": "equipment", + "img": "systems/slazero2/assets/SLA_Assets/Gear/Medikit Disposable.png", + "system": { + "description": "A pack of field dressings and pressure bandages. Can stop a bleeding condition with a Healing roll (difficulty 1). No bonus to Healing checks \u2014 just the materials to work with. Each pack contains 5 dressings.", + "isDrug": false, + "quantity": 5, + "cost": 15, + "weight": 0, + "active": false, + "activeDuration": "", + "activeLabel": "", + "activeNotes": "", + "withdrawalActive": false, + "withdrawalDuration": "", + "withdrawalLabel": "", + "withdrawalNotes": "", + "healthMod": 0, + "resolveMod": 0, + "statPhysicalMod": 0, + "skillAllMod": 0, + "panicReduction": 0, + "injuryPenaltyIgnore": 0, + "stressRecoveryBonus": 0, + "postUseStaminaDamage": 0, + "wdHealthMod": 0, + "wdResolveMod": 0, + "wdStatPhysicalMod": 0, + "wdSkillAllMod": 0, + "wdPanicReduction": 0, + "wdInjuryPenaltyIgnore": 0, + "wdStressRecoveryBonus": 0, + "wdPostUseStaminaDamage": 0, + "skillMods": { + "force": 0, + "melee": 0, + "stamina": 0, + "marksmanship": 0, + "mobility": 0, + "stealth": 0, + "crafting": 0, + "observation": 0, + "survival": 0, + "healing": 0, + "insight": 0, + "persuasion": 0 + }, + "wdSkillMods": { + "force": 0, + "melee": 0, + "stamina": 0, + "marksmanship": 0, + "mobility": 0, + "stealth": 0, + "crafting": 0, + "observation": 0, + "survival": 0, + "healing": 0, + "insight": 0, + "persuasion": 0 + }, + "initiativeMod": 0, + "equipped": false + } + }, + { + "name": "Cutting Charge", + "type": "equipment", + "img": "systems/slazero2/assets/SLA_Assets/Gear/Breaching Frame Charge.png", + "system": { + "description": "A shaped explosive charge designed for breaching. Adheres to any surface and detonates directionally. Cuts through a standard door in one detonation. Armoured doors require a Crafting roll (difficulty 2). Timer or remote detonation.", + "isDrug": false, + "quantity": 1, + "cost": 120, + "weight": 1, + "active": false, + "activeDuration": "", + "activeLabel": "", + "activeNotes": "", + "withdrawalActive": false, + "withdrawalDuration": "", + "withdrawalLabel": "", + "withdrawalNotes": "", + "healthMod": 0, + "resolveMod": 0, + "statPhysicalMod": 0, + "skillAllMod": 0, + "panicReduction": 0, + "injuryPenaltyIgnore": 0, + "stressRecoveryBonus": 0, + "postUseStaminaDamage": 0, + "wdHealthMod": 0, + "wdResolveMod": 0, + "wdStatPhysicalMod": 0, + "wdSkillAllMod": 0, + "wdPanicReduction": 0, + "wdInjuryPenaltyIgnore": 0, + "wdStressRecoveryBonus": 0, + "wdPostUseStaminaDamage": 0, + "skillMods": { + "force": 0, + "melee": 0, + "stamina": 0, + "marksmanship": 0, + "mobility": 0, + "stealth": 0, + "crafting": 0, + "observation": 0, + "survival": 0, + "healing": 0, + "insight": 0, + "persuasion": 0 + }, + "wdSkillMods": { + "force": 0, + "melee": 0, + "stamina": 0, + "marksmanship": 0, + "mobility": 0, + "stealth": 0, + "crafting": 0, + "observation": 0, + "survival": 0, + "healing": 0, + "insight": 0, + "persuasion": 0 + }, + "initiativeMod": 0, + "equipped": false + } + }, + { + "name": "Klippo Multi-Band Communicator", + "type": "equipment", + "img": "systems/slazero2/assets/SLA_Assets/Gear/Klippo MultiBand Comm.png", + "system": { + "description": "A military-grade encrypted multi-band radio. Communicates on SLA operative frequencies, civilian bands, and emergency channels. Encrypted transmissions cannot be monitored without SLA-level decryption hardware.", + "isDrug": false, + "quantity": 1, + "cost": 200, + "weight": 1, + "active": false, + "activeDuration": "", + "activeLabel": "", + "activeNotes": "", + "withdrawalActive": false, + "withdrawalDuration": "", + "withdrawalLabel": "", + "withdrawalNotes": "", + "healthMod": 0, + "resolveMod": 0, + "statPhysicalMod": 0, + "skillAllMod": 0, + "panicReduction": 0, + "injuryPenaltyIgnore": 0, + "stressRecoveryBonus": 0, + "postUseStaminaDamage": 0, + "wdHealthMod": 0, + "wdResolveMod": 0, + "wdStatPhysicalMod": 0, + "wdSkillAllMod": 0, + "wdPanicReduction": 0, + "wdInjuryPenaltyIgnore": 0, + "wdStressRecoveryBonus": 0, + "wdPostUseStaminaDamage": 0, + "skillMods": { + "force": 0, + "melee": 0, + "stamina": 0, + "marksmanship": 0, + "mobility": 0, + "stealth": 0, + "crafting": 0, + "observation": 1, + "survival": 0, + "healing": 0, + "insight": 0, + "persuasion": 0 + }, + "wdSkillMods": { + "force": 0, + "melee": 0, + "stamina": 0, + "marksmanship": 0, + "mobility": 0, + "stealth": 0, + "crafting": 0, + "observation": 0, + "survival": 0, + "healing": 0, + "insight": 0, + "persuasion": 0 + }, + "initiativeMod": 0, + "equipped": false + } + }, + { + "name": "Disposable Camera Drone", + "type": "equipment", + "img": "systems/slazero2/assets/SLA_Assets/Gear/Motion Tracker.png", + "system": { + "description": "A small quadrotor reconnaissance drone. Controlled via wrist unit, feeds live video to the operator. Silent at low altitude. Range 200m. Battery lasts 20 minutes. Disposable \u2014 not intended for recovery. +2 dice on Observation rolls when drone is deployed.", + "isDrug": false, + "quantity": 1, + "cost": 180, + "weight": 1, + "active": false, + "activeDuration": "1 scene (20 min)", + "activeLabel": "Drone Deployed", + "activeNotes": "+2 dice Observation. Covers areas the operative cannot physically access.", + "withdrawalActive": false, + "withdrawalDuration": "", + "withdrawalLabel": "", + "withdrawalNotes": "", + "healthMod": 0, + "resolveMod": 0, + "statPhysicalMod": 0, + "skillAllMod": 0, + "panicReduction": 0, + "injuryPenaltyIgnore": 0, + "stressRecoveryBonus": 0, + "postUseStaminaDamage": 0, + "wdHealthMod": 0, + "wdResolveMod": 0, + "wdStatPhysicalMod": 0, + "wdSkillAllMod": 0, + "wdPanicReduction": 0, + "wdInjuryPenaltyIgnore": 0, + "wdStressRecoveryBonus": 0, + "wdPostUseStaminaDamage": 0, + "skillMods": { + "force": 0, + "melee": 0, + "stamina": 0, + "marksmanship": 0, + "mobility": 0, + "stealth": 0, + "crafting": 0, + "observation": 2, + "survival": 0, + "healing": 0, + "insight": 0, + "persuasion": 0 + }, + "wdSkillMods": { + "force": 0, + "melee": 0, + "stamina": 0, + "marksmanship": 0, + "mobility": 0, + "stealth": 0, + "crafting": 0, + "observation": 0, + "survival": 0, + "healing": 0, + "insight": 0, + "persuasion": 0 + }, + "initiativeMod": 0, + "equipped": false + } + }, + { + "name": "Field Tool Kit", + "type": "equipment", + "img": "systems/slazero2/assets/SLA_Assets/Gear/Tool Kit General.png", + "system": { + "description": "A compact tool roll containing multi-tools, wire cutters, lock picks, diagnostic scanner, and patch materials. Required for most Crafting rolls involving mechanical or electronic systems. Without a toolkit, Crafting checks are at -2 dice.", + "isDrug": false, + "quantity": 1, + "cost": 150, + "weight": 2, + "active": false, + "activeDuration": "", + "activeLabel": "", + "activeNotes": "", + "withdrawalActive": false, + "withdrawalDuration": "", + "withdrawalLabel": "", + "withdrawalNotes": "", + "healthMod": 0, + "resolveMod": 0, + "statPhysicalMod": 0, + "skillAllMod": 0, + "panicReduction": 0, + "injuryPenaltyIgnore": 0, + "stressRecoveryBonus": 0, + "postUseStaminaDamage": 0, + "wdHealthMod": 0, + "wdResolveMod": 0, + "wdStatPhysicalMod": 0, + "wdSkillAllMod": 0, + "wdPanicReduction": 0, + "wdInjuryPenaltyIgnore": 0, + "wdStressRecoveryBonus": 0, + "wdPostUseStaminaDamage": 0, + "skillMods": { + "force": 0, + "melee": 0, + "stamina": 0, + "marksmanship": 0, + "mobility": 0, + "stealth": 0, + "crafting": 2, + "observation": 0, + "survival": 0, + "healing": 0, + "insight": 0, + "persuasion": 0 + }, + "wdSkillMods": { + "force": 0, + "melee": 0, + "stamina": 0, + "marksmanship": 0, + "mobility": 0, + "stealth": 0, + "crafting": 0, + "observation": 0, + "survival": 0, + "healing": 0, + "insight": 0, + "persuasion": 0 + }, + "initiativeMod": 0, + "equipped": false + } + }, + { + "name": "Encrypted Comms Unit", + "type": "equipment", + "img": "systems/slazero2/assets/SLA_Assets/Gear/Headset Comms.png", + "system": { + "description": "A compact earpiece and throat mic with encrypted SLA-frequency transmission. Squad-range communication without risk of civilian monitoring. Standard issue for most SLA operative squads.", + "isDrug": false, + "quantity": 1, + "cost": 80, + "weight": 0, + "active": false, + "activeDuration": "", + "activeLabel": "", + "activeNotes": "", + "withdrawalActive": false, + "withdrawalDuration": "", + "withdrawalLabel": "", + "withdrawalNotes": "", + "healthMod": 0, + "resolveMod": 0, + "statPhysicalMod": 0, + "skillAllMod": 0, + "panicReduction": 0, + "injuryPenaltyIgnore": 0, + "stressRecoveryBonus": 0, + "postUseStaminaDamage": 0, + "wdHealthMod": 0, + "wdResolveMod": 0, + "wdStatPhysicalMod": 0, + "wdSkillAllMod": 0, + "wdPanicReduction": 0, + "wdInjuryPenaltyIgnore": 0, + "wdStressRecoveryBonus": 0, + "wdPostUseStaminaDamage": 0, + "skillMods": { + "force": 0, + "melee": 0, + "stamina": 0, + "marksmanship": 0, + "mobility": 0, + "stealth": 0, + "crafting": 0, + "observation": 0, + "survival": 0, + "healing": 0, + "insight": 0, + "persuasion": 0 + }, + "wdSkillMods": { + "force": 0, + "melee": 0, + "stamina": 0, + "marksmanship": 0, + "mobility": 0, + "stealth": 0, + "crafting": 0, + "observation": 0, + "survival": 0, + "healing": 0, + "insight": 0, + "persuasion": 0 + }, + "initiativeMod": 0, + "equipped": false + } + }, + { + "name": "UV/Multi-Spectrum Torch", + "type": "equipment", + "img": "systems/slazero2/assets/SLA_Assets/Gear/UV Multi Spectrum Torch.png", + "system": { + "description": "A compact torch with UV, infrared, and visible-light modes. UV mode reveals forensic evidence, blood trails, and certain biogenetic signatures. IR mode functions as basic night vision at Short range. +1 die to Observation in low-light conditions.", + "isDrug": false, + "quantity": 1, + "cost": 60, + "weight": 0, + "active": false, + "activeDuration": "", + "activeLabel": "", + "activeNotes": "", + "withdrawalActive": false, + "withdrawalDuration": "", + "withdrawalLabel": "", + "withdrawalNotes": "", + "healthMod": 0, + "resolveMod": 0, + "statPhysicalMod": 0, + "skillAllMod": 0, + "panicReduction": 0, + "injuryPenaltyIgnore": 0, + "stressRecoveryBonus": 0, + "postUseStaminaDamage": 0, + "wdHealthMod": 0, + "wdResolveMod": 0, + "wdStatPhysicalMod": 0, + "wdSkillAllMod": 0, + "wdPanicReduction": 0, + "wdInjuryPenaltyIgnore": 0, + "wdStressRecoveryBonus": 0, + "wdPostUseStaminaDamage": 0, + "skillMods": { + "force": 0, + "melee": 0, + "stamina": 0, + "marksmanship": 0, + "mobility": 0, + "stealth": 0, + "crafting": 0, + "observation": 1, + "survival": 0, + "healing": 0, + "insight": 0, + "persuasion": 0 + }, + "wdSkillMods": { + "force": 0, + "melee": 0, + "stamina": 0, + "marksmanship": 0, + "mobility": 0, + "stealth": 0, + "crafting": 0, + "observation": 0, + "survival": 0, + "healing": 0, + "insight": 0, + "persuasion": 0 + }, + "initiativeMod": 0, + "equipped": false + } + }, + { + "name": "Oyster Card (ITB Access)", + "type": "equipment", + "img": "systems/slazero2/assets/SLA_Assets/Gear/Oyster Card.png", + "system": { + "description": "An ITB-issued corporate access card. Grants entry to ITB-managed facilities, transit networks, and SLA approved zones. Cards can be cloned, forged, or reprogrammed with a Crafting roll (difficulty 3). A real one is worth considerably more than what ITB charges for it.", + "isDrug": false, + "quantity": 1, + "cost": 50, + "weight": 0, + "active": false, + "activeDuration": "", + "activeLabel": "", + "activeNotes": "", + "withdrawalActive": false, + "withdrawalDuration": "", + "withdrawalLabel": "", + "withdrawalNotes": "", + "healthMod": 0, + "resolveMod": 0, + "statPhysicalMod": 0, + "skillAllMod": 0, + "panicReduction": 0, + "injuryPenaltyIgnore": 0, + "stressRecoveryBonus": 0, + "postUseStaminaDamage": 0, + "wdHealthMod": 0, + "wdResolveMod": 0, + "wdStatPhysicalMod": 0, + "wdSkillAllMod": 0, + "wdPanicReduction": 0, + "wdInjuryPenaltyIgnore": 0, + "wdStressRecoveryBonus": 0, + "wdPostUseStaminaDamage": 0, + "skillMods": { + "force": 0, + "melee": 0, + "stamina": 0, + "marksmanship": 0, + "mobility": 0, + "stealth": 0, + "crafting": 0, + "observation": 0, + "survival": 0, + "healing": 0, + "insight": 0, + "persuasion": 0 + }, + "wdSkillMods": { + "force": 0, + "melee": 0, + "stamina": 0, + "marksmanship": 0, + "mobility": 0, + "stealth": 0, + "crafting": 0, + "observation": 0, + "survival": 0, + "healing": 0, + "insight": 0, + "persuasion": 0 + }, + "initiativeMod": 0, + "equipped": false + } + }, + { + "name": "Duct Tape (Heavy Duty)", + "type": "equipment", + "img": "systems/slazero2/assets/SLA_Assets/Gear/Duct Tape Roll.png", + "system": { + "description": "Three metres of military-grade adhesive tape. Can temporarily seal wounds (stops Bleeding as an action, no roll required), restrain a prisoner, patch a breach suit, or fix a vehicle component. The most useful item in any kit, according to every veteran who's ever survived.", + "isDrug": false, + "quantity": 1, + "cost": 5, + "weight": 0, + "active": false, + "activeDuration": "", + "activeLabel": "", + "activeNotes": "", + "withdrawalActive": false, + "withdrawalDuration": "", + "withdrawalLabel": "", + "withdrawalNotes": "", + "healthMod": 0, + "resolveMod": 0, + "statPhysicalMod": 0, + "skillAllMod": 0, + "panicReduction": 0, + "injuryPenaltyIgnore": 0, + "stressRecoveryBonus": 0, + "postUseStaminaDamage": 0, + "wdHealthMod": 0, + "wdResolveMod": 0, + "wdStatPhysicalMod": 0, + "wdSkillAllMod": 0, + "wdPanicReduction": 0, + "wdInjuryPenaltyIgnore": 0, + "wdStressRecoveryBonus": 0, + "wdPostUseStaminaDamage": 0, + "skillMods": { + "force": 0, + "melee": 0, + "stamina": 0, + "marksmanship": 0, + "mobility": 0, + "stealth": 0, + "crafting": 0, + "observation": 0, + "survival": 0, + "healing": 0, + "insight": 0, + "persuasion": 0 + }, + "wdSkillMods": { + "force": 0, + "melee": 0, + "stamina": 0, + "marksmanship": 0, + "mobility": 0, + "stealth": 0, + "crafting": 0, + "observation": 0, + "survival": 0, + "healing": 0, + "insight": 0, + "persuasion": 0 + }, + "initiativeMod": 0, + "equipped": false + } + } +] \ No newline at end of file diff --git a/packs/sla-ammo/000004.log b/packs/sla-ammo/000004.log new file mode 100644 index 0000000..e69de29 diff --git a/packs/sla-ammo/000005.ldb b/packs/sla-ammo/000005.ldb new file mode 100644 index 0000000..a5cf64d Binary files /dev/null and b/packs/sla-ammo/000005.ldb differ diff --git a/packs/sla-ammo/CURRENT b/packs/sla-ammo/CURRENT new file mode 100644 index 0000000..1a84852 --- /dev/null +++ b/packs/sla-ammo/CURRENT @@ -0,0 +1 @@ +MANIFEST-000002 diff --git a/packs/sla-ammo/LOG b/packs/sla-ammo/LOG new file mode 100644 index 0000000..d7c02b5 --- /dev/null +++ b/packs/sla-ammo/LOG @@ -0,0 +1,5 @@ +2026/07/14-14:50:05.999990 16fb67000 Delete type=3 #1 +2026/07/14-14:50:06.000962 16fbf3000 Level-0 table #5: started +2026/07/14-14:50:06.001186 16fbf3000 Level-0 table #5: 4097 bytes OK +2026/07/14-14:50:06.001278 16fbf3000 Delete type=0 #3 +2026/07/14-14:50:06.001454 16fbf3000 Manual compaction at level-0 from '\x00' @ 72057594037927935 : 1 .. '\xc3\xbf' @ 0 : 0; will stop at (end) diff --git a/packs/sla-ammo/MANIFEST-000002 b/packs/sla-ammo/MANIFEST-000002 new file mode 100644 index 0000000..0495c92 Binary files /dev/null and b/packs/sla-ammo/MANIFEST-000002 differ diff --git a/packs/sla-armors/000004.log b/packs/sla-armors/000004.log new file mode 100644 index 0000000..e69de29 diff --git a/packs/sla-armors/000005.ldb b/packs/sla-armors/000005.ldb new file mode 100644 index 0000000..6a313ed Binary files /dev/null and b/packs/sla-armors/000005.ldb differ diff --git a/packs/sla-armors/CURRENT b/packs/sla-armors/CURRENT new file mode 100644 index 0000000..1a84852 --- /dev/null +++ b/packs/sla-armors/CURRENT @@ -0,0 +1 @@ +MANIFEST-000002 diff --git a/packs/sla-armors/LOG b/packs/sla-armors/LOG new file mode 100644 index 0000000..10de086 --- /dev/null +++ b/packs/sla-armors/LOG @@ -0,0 +1,5 @@ +2026/07/14-14:50:05.980778 16e343000 Delete type=3 #1 +2026/07/14-14:50:05.982731 16fbf3000 Level-0 table #5: started +2026/07/14-14:50:05.983168 16fbf3000 Level-0 table #5: 5659 bytes OK +2026/07/14-14:50:05.983335 16fbf3000 Delete type=0 #3 +2026/07/14-14:50:05.983436 16fbf3000 Manual compaction at level-0 from '\x00' @ 72057594037927935 : 1 .. '\xc3\xbf' @ 0 : 0; will stop at (end) diff --git a/packs/sla-armors/MANIFEST-000002 b/packs/sla-armors/MANIFEST-000002 new file mode 100644 index 0000000..bbc73db Binary files /dev/null and b/packs/sla-armors/MANIFEST-000002 differ diff --git a/packs/sla-drugs/000004.log b/packs/sla-drugs/000004.log new file mode 100644 index 0000000..e69de29 diff --git a/packs/sla-drugs/000005.ldb b/packs/sla-drugs/000005.ldb new file mode 100644 index 0000000..5409f6e Binary files /dev/null and b/packs/sla-drugs/000005.ldb differ diff --git a/packs/sla-drugs/CURRENT b/packs/sla-drugs/CURRENT new file mode 100644 index 0000000..1a84852 --- /dev/null +++ b/packs/sla-drugs/CURRENT @@ -0,0 +1 @@ +MANIFEST-000002 diff --git a/packs/sla-drugs/LOG b/packs/sla-drugs/LOG new file mode 100644 index 0000000..07a91ae --- /dev/null +++ b/packs/sla-drugs/LOG @@ -0,0 +1,5 @@ +2026/07/14-14:50:05.985512 16fb67000 Delete type=3 #1 +2026/07/14-14:50:05.986259 16fbf3000 Level-0 table #5: started +2026/07/14-14:50:05.986624 16fbf3000 Level-0 table #5: 13222 bytes OK +2026/07/14-14:50:05.986706 16fbf3000 Delete type=0 #3 +2026/07/14-14:50:05.986759 16fbf3000 Manual compaction at level-0 from '\x00' @ 72057594037927935 : 1 .. '\xc3\xbf' @ 0 : 0; will stop at (end) diff --git a/packs/sla-drugs/MANIFEST-000002 b/packs/sla-drugs/MANIFEST-000002 new file mode 100644 index 0000000..472eba7 Binary files /dev/null and b/packs/sla-drugs/MANIFEST-000002 differ diff --git a/packs/sla-ebb-formulae/000004.log b/packs/sla-ebb-formulae/000004.log new file mode 100644 index 0000000..e69de29 diff --git a/packs/sla-ebb-formulae/000005.ldb b/packs/sla-ebb-formulae/000005.ldb new file mode 100644 index 0000000..2c2d66d Binary files /dev/null and b/packs/sla-ebb-formulae/000005.ldb differ diff --git a/packs/sla-ebb-formulae/CURRENT b/packs/sla-ebb-formulae/CURRENT new file mode 100644 index 0000000..1a84852 --- /dev/null +++ b/packs/sla-ebb-formulae/CURRENT @@ -0,0 +1 @@ +MANIFEST-000002 diff --git a/packs/sla-ebb-formulae/LOG b/packs/sla-ebb-formulae/LOG new file mode 100644 index 0000000..092f4db --- /dev/null +++ b/packs/sla-ebb-formulae/LOG @@ -0,0 +1,5 @@ +2026/07/14-14:50:05.988527 16f35b000 Delete type=3 #1 +2026/07/14-14:50:05.989609 16fbf3000 Level-0 table #5: started +2026/07/14-14:50:05.989991 16fbf3000 Level-0 table #5: 15385 bytes OK +2026/07/14-14:50:05.990080 16fbf3000 Delete type=0 #3 +2026/07/14-14:50:05.990129 16fbf3000 Manual compaction at level-0 from '\x00' @ 72057594037927935 : 1 .. '\xc3\xbf' @ 0 : 0; will stop at (end) diff --git a/packs/sla-ebb-formulae/MANIFEST-000002 b/packs/sla-ebb-formulae/MANIFEST-000002 new file mode 100644 index 0000000..289874e Binary files /dev/null and b/packs/sla-ebb-formulae/MANIFEST-000002 differ diff --git a/packs/sla-equipment/000004.log b/packs/sla-equipment/000004.log new file mode 100644 index 0000000..e69de29 diff --git a/packs/sla-equipment/000005.ldb b/packs/sla-equipment/000005.ldb new file mode 100644 index 0000000..e938798 Binary files /dev/null and b/packs/sla-equipment/000005.ldb differ diff --git a/packs/sla-equipment/CURRENT b/packs/sla-equipment/CURRENT new file mode 100644 index 0000000..1a84852 --- /dev/null +++ b/packs/sla-equipment/CURRENT @@ -0,0 +1 @@ +MANIFEST-000002 diff --git a/packs/sla-equipment/LOG b/packs/sla-equipment/LOG new file mode 100644 index 0000000..27d7225 --- /dev/null +++ b/packs/sla-equipment/LOG @@ -0,0 +1,5 @@ +2026/07/14-14:50:05.997512 16eb4f000 Delete type=3 #1 +2026/07/14-14:50:05.997924 16fbf3000 Level-0 table #5: started +2026/07/14-14:50:05.998107 16fbf3000 Level-0 table #5: 7003 bytes OK +2026/07/14-14:50:05.998179 16fbf3000 Delete type=0 #3 +2026/07/14-14:50:05.998226 16fbf3000 Manual compaction at level-0 from '\x00' @ 72057594037927935 : 1 .. '\xc3\xbf' @ 0 : 0; will stop at (end) diff --git a/packs/sla-equipment/MANIFEST-000002 b/packs/sla-equipment/MANIFEST-000002 new file mode 100644 index 0000000..10569ff Binary files /dev/null and b/packs/sla-equipment/MANIFEST-000002 differ diff --git a/packs/sla-specialties/000004.log b/packs/sla-specialties/000004.log new file mode 100644 index 0000000..e69de29 diff --git a/packs/sla-specialties/000005.ldb b/packs/sla-specialties/000005.ldb new file mode 100644 index 0000000..a710bcb Binary files /dev/null and b/packs/sla-specialties/000005.ldb differ diff --git a/packs/sla-specialties/CURRENT b/packs/sla-specialties/CURRENT new file mode 100644 index 0000000..1a84852 --- /dev/null +++ b/packs/sla-specialties/CURRENT @@ -0,0 +1 @@ +MANIFEST-000002 diff --git a/packs/sla-specialties/LOG b/packs/sla-specialties/LOG new file mode 100644 index 0000000..29f8b4e --- /dev/null +++ b/packs/sla-specialties/LOG @@ -0,0 +1,5 @@ +2026/07/14-14:50:05.991885 16eb4f000 Delete type=3 #1 +2026/07/14-14:50:05.992599 16fbf3000 Level-0 table #5: started +2026/07/14-14:50:05.992855 16fbf3000 Level-0 table #5: 7095 bytes OK +2026/07/14-14:50:05.992966 16fbf3000 Delete type=0 #3 +2026/07/14-14:50:05.993027 16fbf3000 Manual compaction at level-0 from '\x00' @ 72057594037927935 : 1 .. '\xc3\xbf' @ 0 : 0; will stop at (end) diff --git a/packs/sla-specialties/MANIFEST-000002 b/packs/sla-specialties/MANIFEST-000002 new file mode 100644 index 0000000..5958dba Binary files /dev/null and b/packs/sla-specialties/MANIFEST-000002 differ diff --git a/packs/sla-weapons/000004.log b/packs/sla-weapons/000004.log new file mode 100644 index 0000000..e69de29 diff --git a/packs/sla-weapons/000005.ldb b/packs/sla-weapons/000005.ldb new file mode 100644 index 0000000..06bc716 Binary files /dev/null and b/packs/sla-weapons/000005.ldb differ diff --git a/packs/sla-weapons/CURRENT b/packs/sla-weapons/CURRENT new file mode 100644 index 0000000..1a84852 --- /dev/null +++ b/packs/sla-weapons/CURRENT @@ -0,0 +1 @@ +MANIFEST-000002 diff --git a/packs/sla-weapons/LOG b/packs/sla-weapons/LOG new file mode 100644 index 0000000..6660639 --- /dev/null +++ b/packs/sla-weapons/LOG @@ -0,0 +1,5 @@ +2026/07/14-14:50:05.994668 16eb4f000 Delete type=3 #1 +2026/07/14-14:50:05.995664 16fbf3000 Level-0 table #5: started +2026/07/14-14:50:05.995978 16fbf3000 Level-0 table #5: 14919 bytes OK +2026/07/14-14:50:05.996067 16fbf3000 Delete type=0 #3 +2026/07/14-14:50:05.996129 16fbf3000 Manual compaction at level-0 from '\x00' @ 72057594037927935 : 1 .. '\xc3\xbf' @ 0 : 0; will stop at (end) diff --git a/packs/sla-weapons/MANIFEST-000002 b/packs/sla-weapons/MANIFEST-000002 new file mode 100644 index 0000000..97002d7 Binary files /dev/null and b/packs/sla-weapons/MANIFEST-000002 differ diff --git a/packs/specialties.json b/packs/specialties.json new file mode 100644 index 0000000..f05a112 --- /dev/null +++ b/packs/specialties.json @@ -0,0 +1,226 @@ +[ + { + "name": "Sniper", + "type": "specialty", + "img": "systems/slazero2/assets/icons/specialties/specialty-sniper.svg", + "system": { + "description": "You are trained to wait, breathe, and kill from extreme range, using concealment and patience instead of volume of fire.", + "effects": "+2 Marksmanship (Long+ range, concealment required)", + "category": "combat", + "package": "Scout", + "prerequisites": "Scout training package", + "isActive": true, + "effect": "When attacking from Long or Extreme range while hidden or in concealment, you gain +1 damage on a successful ranged attack." + } + }, + { + "name": "Fast Reflexes", + "type": "specialty", + "img": "systems/slazero2/assets/icons/specialties/specialty-fast-reflexes.svg", + "system": { + "description": "Your body reacts before your brain finishes the thought; you slip out of danger and act a heartbeat before others.", + "effects": "Draw 2 initiative cards, choose the better one", + "category": "combat", + "package": "Scout", + "prerequisites": "Scout training package", + "isActive": true, + "effect": "At the start of each combat, you gain +1 automatic success to Initiative." + } + }, + { + "name": "Inquisitive", + "type": "specialty", + "img": "systems/slazero2/assets/icons/specialties/specialty-inquisitive.svg", + "system": { + "description": "You can\u2019t leave a question alone; you pick at loose threads until they reveal the bigger pattern.", + "effects": "Push WIT rolls twice per roll", + "category": "investigation", + "package": "Investigator", + "prerequisites": "Investigator training package", + "isActive": true, + "effect": "When you spend extra time (a stretch) investigating a scene, system, or document, you automatically gain one additional piece of useful information beyond the basic success result." + } + }, + { + "name": "Gut Feeling", + "type": "specialty", + "img": "systems/slazero2/assets/icons/specialties/specialty-gut-feeling.svg", + "system": { + "description": "You have razor\u2011sharp instincts; your body spots danger before your conscious mind understands it.", + "effects": "Use EMP for Observation (threat detection only)", + "category": "investigation", + "package": "Investigator", + "prerequisites": "Investigator training package", + "isActive": true, + "effect": "Once per encounter, you may ask the GM \u201cIs there an immediate hidden threat here?\u201d and receive an honest yes/no answer plus a short hint if the answer is yes." + } + }, + { + "name": "Hard Hitter", + "type": "specialty", + "img": "systems/slazero2/assets/icons/specialties/specialty-hard-hitter.svg", + "system": { + "description": "You don\u2019t waste effort; every swing or shot is committed, brutal, and aimed to break things.", + "effects": "+1 Melee damage (sacrifice fast action)", + "category": "combat", + "package": "Frother", + "prerequisites": "Frother training package", + "isActive": true, + "effect": "Once per round, when you hit with a melee attack, you may trade your current unused Fast action (if any) to gain +2 damage on that attack." + } + }, + { + "name": "True Grit", + "type": "specialty", + "img": "systems/slazero2/assets/icons/specialties/specialty-true-grit.svg", + "system": { + "description": "You simply refuse to stay down; pain and blood loss slow you, but they don\u2019t stop you.", + "effects": "+1 maximum Health", + "category": "survival", + "package": "Frother", + "prerequisites": "Frother training package", + "isActive": true, + "effect": "Increase your maximum Health by +1, and once per combat when you would be Broken by physical damage, you instead remain at 1 Health." + } + }, + { + "name": "Menacing", + "type": "specialty", + "img": "systems/slazero2/assets/icons/specialties/specialty-menacing.svg", + "system": { + "description": "Your presence, tone, and body language radiate controlled threat; people know you mean everything you say.", + "effects": "Use STR for Persuasion (intimidation only)", + "category": "social", + "package": "Corporate", + "prerequisites": "Corporate training package", + "isActive": true, + "effect": "When you use Persuasion to threaten, intimidate, or extract compliance, roll it using STR or EMP (your choice)." + } + }, + { + "name": "Compassion", + "type": "specialty", + "img": "systems/slazero2/assets/icons/specialties/specialty-compassion.svg", + "system": { + "description": "You genuinely care, and people feel it; that makes them open up, calm down, or cling to you in crisis.", + "effects": "Push EMP rolls twice per roll", + "category": "social", + "package": "Corporate", + "prerequisites": "Corporate training package", + "isActive": true, + "effect": "When you successfully calm, reassure, or counsel an NPC or ally (Persuasion or Insight), that target recovers +1 extra Stress beyond the normal recovery from the scene." + } + }, + { + "name": "Field Surgeon", + "type": "specialty", + "img": "systems/slazero2/assets/icons/specialties/specialty-field-surgeon.svg", + "system": { + "description": "You\u2019re trained to work fast in filthy alleys and fire\u2011lit corridors, keeping people alive with whatever is at hand.", + "effects": "+1 Healing (critical injuries only)", + "category": "medical", + "package": "Medic", + "prerequisites": "Medic/Field Surgeon training package", + "isActive": true, + "effect": "When you treat a physical Critical Injury with Healing, reduce the required healing time by one step (rounds \u2192 stretch, stretch \u2192 shift, shift \u2192 2 shifts, etc., to a minimum of 1 stretch)." + } + }, + { + "name": "Healer", + "type": "specialty", + "img": "systems/slazero2/assets/icons/specialties/specialty-healer.svg", + "system": { + "description": "You focus on stabilizing and restoring others, making the most out of limited supplies and time.", + "effects": "Critical injuries heal in half the time", + "category": "medical", + "package": "Medic", + "prerequisites": "Medic/Field Surgeon training package", + "isActive": true, + "effect": "Whenever you successfully restore Health to another character (by Ebb, drugs, or medical treatment), the target recovers +1 additional Health." + } + }, + { + "name": "Ebb Sensitive", + "type": "specialty", + "img": "systems/slazero2/assets/icons/specialties/specialty-ebb-sensitive.svg", + "system": { + "description": "You have an innate feel for flux; the Ebb whispers in your nerves even when you are not actively using it.", + "effects": "Sense Ebons within ~10m radius", + "category": "ebb", + "package": "Ebon - Any", + "prerequisites": "Ebon race", + "isActive": true, + "effect": "You automatically sense the presence and general direction of Ebons or Brain Wasters within Short range and whether Ebb has been used in the area within the last few hours." + } + }, + { + "name": "Ebb Fury", + "type": "specialty", + "img": "systems/slazero2/assets/icons/specialties/specialty-ebb-fury.svg", + "system": { + "description": "You channel rage into raw psychic force, driving your powers harder than is strictly safe.", + "effects": "Spend 1 EP: +2 dice to combat roll", + "category": "ebb", + "package": "Brain Waster - Operative", + "prerequisites": "Brain Waster race", + "isActive": true, + "effect": "Once per combat round, after you roll a successful Ebb Use attack that deals damage, you may add +1 damage to that attack." + } + }, + { + "name": "Code of Honor", + "type": "specialty", + "img": "systems/slazero2/assets/icons/specialties/specialty-code-of-honor.svg", + "system": { + "description": "You live by a strict personal or cultural code; breaking it would be worse than death.", + "effects": "Reroll all dice when upholding honor (once per session)", + "category": "racial", + "package": "Shaktar", + "prerequisites": "Shaktar race", + "isActive": true, + "effect": "When acting directly in line with your code (GM\u2019s approval), you gain +1 die to a single roll once per scene; if you knowingly break your code, you immediately gain +1 Stress." + } + }, + { + "name": "Cold Adapted", + "type": "specialty", + "img": "systems/slazero2/assets/icons/specialties/specialty-cold-adapted.svg", + "system": { + "description": "Your body is made for freezing climates; Mort\u2019s bitter winds feel like mild weather.", + "effects": "Ignore cold penalties; +1 Stealth (dim/dark)", + "category": "racial", + "package": "Wraith Raider", + "prerequisites": "Wraith Raider race", + "isActive": true, + "effect": "You ignore penalties from natural cold environments and gain +1 die to Stealth in dim or dark conditions outdoors." + } + }, + { + "name": "Regeneration", + "type": "specialty", + "img": "systems/slazero2/assets/icons/specialties/specialty-regeneration.svg", + "system": { + "description": "Your body knits flesh and bone far faster than humans; wounds close into angry scars in hours.", + "effects": "Heal 1 Health per hour (automatic)", + "category": "racial", + "package": "Stormer", + "prerequisites": "Stormer race", + "isActive": true, + "effect": "When you rest for at least one stretch in a safe place, you automatically regain +1 additional Health beyond normal recovery." + } + }, + { + "name": "Combat Drug User", + "type": "specialty", + "img": "systems/slazero2/assets/icons/specialties/specialty-combat-drug-user.svg", + "system": { + "description": "You know exactly how to ride the razor edge of combat drugs without immediately burning out.", + "effects": "Rush drug: +2 combat, -2 mental (addiction risk)", + "category": "racial", + "package": "Frother", + "prerequisites": "Frother race", + "isActive": true, + "effect": "When you take a combat drug (such as Rush), you may ignore the first \u20131 die penalty from its side effects for the duration of that dose." + } + } +] \ No newline at end of file diff --git a/packs/tables-data.json b/packs/tables-data.json new file mode 100644 index 0000000..6431300 --- /dev/null +++ b/packs/tables-data.json @@ -0,0 +1,187 @@ +{ + "panic": { + "name": "Panic Table", + "description": "Roll 1d6 + Stress. Results of 7-15 trigger panic effects.", + "formula": "1d6+@stress", + "results": [ + { + "range": [7, 7], + "text": "Nervous Twitch: Gain +1 Stress. All allies in Short range gain +1 Stress as your tension spikes the squad.", + "type": 0 + }, + { + "range": [8, 8], + "text": "Tremble: Until end of scene: -2 dice to all AGI-based rolls (Marksmanship, Mobility, Stealth).", + "type": 0 + }, + { + "range": [9, 9], + "text": "Fumble/Jam: GM chooses one: drop held item; weapon jams (1 Fast action to clear); comms/camera/evidence mishandled (next related roll -2 dice).", + "type": 0 + }, + { + "range": [10, 10], + "text": "Freeze: Lose your next action (combat: lose next turn). You may defend normally but cannot Push again until after you act.", + "type": 0 + }, + { + "range": [11, 11], + "text": "Seek Cover: Use next action moving to cover/safer position if possible. If you refuse, take +1 Stress at start of each round until you comply.", + "type": 0 + }, + { + "range": [12, 12], + "text": "Outburst on Camera: You shout/scream/swear. Lose next action; position revealed (enemies +2 dice to locate/target). If witnesses/Third Eye present, GM introduces 'bad footage' complication.", + "type": 0 + }, + { + "range": [13, 13], + "text": "Flight Response: Must retreat from threat for at least 1 full round or one zone away. During this cannot take Slow actions.", + "type": 0 + }, + { + "range": [14, 14], + "text": "Operative Breakdown: Choose: BERSERK (attack nearest creature next action, friend/foe) OR CATATONIC (incapacitated D6 rounds). Allies who witness gain +1 Stress.", + "type": 0 + }, + { + "range": [15, 99], + "text": "Mind Fracture: Immediately suffer a Mental Critical Injury (roll Mental Critical Table) and apply it now.", + "type": 2, + "collection": "mentalCritical" + } + ] + }, + "physicalCritical": { + "name": "Physical Critical Injuries (D66)", + "description": "Roll 2d6 - first die is tens, second is ones (shown as tens.ones)", + "formula": "1d6t + 1d6", + "results": [ + {"key": "1.1", "name": "Winded", "lethal": false, "timeLimit": "", "effects": "Lose next Fast action; -1 die to physical rolls for 1 round.", "healing": "1 stretch"}, + {"key": "1.2", "name": "Rattled", "lethal": false, "timeLimit": "", "effects": "-1 die to Marksmanship and Observation for 1 scene.", "healing": "1 shift"}, + {"key": "1.3", "name": "Sprained wrist", "lethal": false, "timeLimit": "", "effects": "-2 dice to actions using that hand (shooting, melee, crafting).", "healing": "D6 shifts"}, + {"key": "1.4", "name": "Sprained ankle", "lethal": false, "timeLimit": "", "effects": "Movement is a Slow action until treated.", "healing": "D6 shifts or Healing"}, + {"key": "1.5", "name": "Scalp cut", "lethal": false, "timeLimit": "", "effects": "Bleeding: take 1 damage per round until Healing roll succeeds (Slow action).", "healing": "1 shift"}, + {"key": "1.6", "name": "Concussion", "lethal": false, "timeLimit": "", "effects": "-2 dice to WIT-based rolls for D6 shifts.", "healing": "D6 shifts"}, + + {"key": "2.1", "name": "Cracked ribs", "lethal": false, "timeLimit": "", "effects": "-2 dice to Mobility; Running costs +1 Stress.", "healing": "2D6 shifts"}, + {"key": "2.2", "name": "Dislocated shoulder", "lethal": false, "timeLimit": "", "effects": "Arm unusable for melee/shooting; can still hold light items.", "healing": "2D6 shifts"}, + {"key": "2.3", "name": "Broken fingers", "lethal": false, "timeLimit": "", "effects": "Reloading requires both actions (Fast+Slow); -2 dice to fine tasks.", "healing": "2D6 shifts"}, + {"key": "2.4", "name": "Broken nose", "lethal": false, "timeLimit": "", "effects": "-2 dice to Persuasion; blood/mask issues impose -1 die on Observation.", "healing": "D6 shifts"}, + {"key": "2.5", "name": "Deep thigh gash", "lethal": false, "timeLimit": "", "effects": "Movement becomes Slow; if you Run, take 1 damage after the move.", "healing": "2D6 shifts"}, + {"key": "2.6", "name": "Hearing loss", "lethal": false, "timeLimit": "", "effects": "-2 dice to Observation checks relying on sound/comms cues.", "healing": "D6 shifts"}, + + {"key": "3.1", "name": "Knocked-out teeth", "lethal": false, "timeLimit": "", "effects": "-2 dice to Persuasion; intimidation attempts +1 die if you show the injury.", "healing": "D6 days"}, + {"key": "3.2", "name": "Crushed toes", "lethal": false, "timeLimit": "", "effects": "Movement Slow; -2 dice to Mobility in cluttered zones.", "healing": "2D6 days"}, + {"key": "3.3", "name": "Gouged eye", "lethal": false, "timeLimit": "", "effects": "-2 dice to Marksmanship and Observation (depth perception).", "healing": "2D6 days or Permanent if untreated"}, + {"key": "3.4", "name": "Busted kneecap", "lethal": false, "timeLimit": "", "effects": "Cannot Run; Movement Slow.", "healing": "2D6 days"}, + {"key": "3.5", "name": "Broken arm", "lethal": false, "timeLimit": "", "effects": "Arm unusable; no 2-handed weapons.", "healing": "2D6 days"}, + {"key": "3.6", "name": "Broken leg", "lethal": false, "timeLimit": "", "effects": "Movement Slow; Retreat requires Mobility roll or fall prone.", "healing": "3D6 days"}, + + {"key": "4.1", "name": "Punctured lung", "lethal": true, "timeLimit": "1 shift", "effects": "-2 dice to Stamina & Mobility; each round you exert yourself take 1 damage.", "healing": "D6 days"}, + {"key": "4.2", "name": "Bleeding gut", "lethal": true, "timeLimit": "1 shift", "effects": "Each Slow action costs 1 damage (internal bleed).", "healing": "D6 days"}, + {"key": "4.3", "name": "Ruptured intestines", "lethal": true, "timeLimit": "1 shift", "effects": "Infection risk: each day roll Stamina; fail = take 1 damage.", "healing": "2D6 days"}, + {"key": "4.4", "name": "Severed arm artery", "lethal": true, "timeLimit": "1 stretch", "effects": "Arm unusable; must be stabilized or death check at time limit.", "healing": "D6 days"}, + {"key": "4.5", "name": "Severed leg artery", "lethal": true, "timeLimit": "1 stretch", "effects": "Movement impossible unaided; must be stabilized.", "healing": "D6 days"}, + {"key": "4.6", "name": "Cracked spine", "lethal": true, "timeLimit": "1 day", "effects": "Collapse prone; cannot Move without help; if not stabilized becomes permanent paralysis.", "healing": "3D6 days or Permanent"}, + + {"key": "5.1", "name": "Ruptured jugular", "lethal": true, "timeLimit": "1 round", "effects": "Immediate stabilize or die at time limit.", "healing": "—"}, + {"key": "5.2", "name": "Ruptured aorta", "lethal": true, "timeLimit": "2 rounds", "effects": "Immediate stabilize or die at time limit.", "healing": "—"}, + {"key": "5.3", "name": "Severed arm", "lethal": true, "timeLimit": "1 shift", "effects": "Permanent loss; immediate +2 Stress from shock.", "healing": "Permanent"}, + {"key": "5.4", "name": "Severed leg", "lethal": true, "timeLimit": "1 shift", "effects": "Permanent loss; you collapse; movement impossible unaided.", "healing": "Permanent"}, + {"key": "5.5", "name": "Impaled heart", "lethal": true, "timeLimit": "1 round", "effects": "You are Broken; only stabilization prevents death.", "healing": "Permanent condition"}, + {"key": "5.6", "name": "Crushed skull", "lethal": true, "timeLimit": "Instant", "effects": "You die immediately.", "healing": "—"}, + + {"key": "6.1", "name": "Disembowelled", "lethal": true, "timeLimit": "Instant", "effects": "You die immediately.", "healing": "—"}, + {"key": "6.2", "name": "Pierced head", "lethal": true, "timeLimit": "Instant", "effects": "You die immediately.", "healing": "—"}, + {"key": "6.3", "name": "Spinal sever", "lethal": true, "timeLimit": "Instant", "effects": "You die immediately (or GM option: permanent paralysis if cinematic).", "healing": "—"}, + {"key": "6.4", "name": "Massive trauma", "lethal": true, "timeLimit": "Instant", "effects": "You die immediately.", "healing": "—"}, + {"key": "6.5", "name": "Total organ failure", "lethal": true, "timeLimit": "Instant", "effects": "You die immediately.", "healing": "—"}, + {"key": "6.6", "name": "Catastrophic destruction", "lethal": true, "timeLimit": "Instant", "effects": "You die immediately.", "healing": "—"} + ] + }, + "mentalCritical": { + "name": "Mental Critical Injuries (D66)", + "description": "Roll 2d6 - first die is tens, second is ones (shown as tens.ones)", + "formula": "1d6t + 1d6", + "results": [ + {"key": "1.1", "name": "Shaken", "lethal": false, "timeLimit": "", "effects": "+1 Stress; -1 die to all rolls for 1 round.", "recovery": "1 stretch"}, + {"key": "1.2", "name": "Tunnel vision", "lethal": false, "timeLimit": "", "effects": "-2 dice to Observation; you miss side details.", "recovery": "1 shift"}, + {"key": "1.3", "name": "Jumpy", "lethal": false, "timeLimit": "", "effects": "Must spend Fast action to Focus before any ranged attack or power use.", "recovery": "D6 shifts"}, + {"key": "1.4", "name": "Intrusive media loop", "lethal": false, "timeLimit": "", "effects": "-2 dice to Insight/Persuasion (can't read people; hear 'commentary').", "recovery": "D6 shifts"}, + {"key": "1.5", "name": "Nausea", "lethal": false, "timeLimit": "", "effects": "First Slow action each scene costs +1 Stress.", "recovery": "D6 shifts"}, + {"key": "1.6", "name": "Dissociation", "lethal": false, "timeLimit": "", "effects": "Cannot Push social rolls; speech becomes flat/odd (roleplay flag).", "recovery": "D6 shifts"}, + + {"key": "2.1", "name": "Anxiety spiral", "lethal": false, "timeLimit": "", "effects": "+1 Stress each shift until safe rest stretch.", "recovery": "D6 shifts"}, + {"key": "2.2", "name": "Nightmares", "lethal": false, "timeLimit": "", "effects": "Sleep doesn't clear Stress unless you succeed Insight roll per sleep shift.", "recovery": "D6 days"}, + {"key": "2.3", "name": "Phobia (triggered)", "lethal": false, "timeLimit": "", "effects": "Near trigger: +1 Stress per round; cannot Push.", "recovery": "2D6 days"}, + {"key": "2.4", "name": "Compulsion: Check kit", "lethal": false, "timeLimit": "", "effects": "Start of each scene: spend 1 Fast action checking kit or take +1 Stress.", "recovery": "D6 days"}, + {"key": "2.5", "name": "Paranoia (no help)", "lethal": false, "timeLimit": "", "effects": "Cannot receive Help dice; reject assistance unless persuaded.", "recovery": "2D6 days"}, + {"key": "2.6", "name": "Emotional shutdown", "lethal": false, "timeLimit": "", "effects": "-2 dice to EMP rolls; can't connect or comfort.", "recovery": "2D6 days"}, + + {"key": "3.1", "name": "Rage trigger", "lethal": false, "timeLimit": "", "effects": "When harmed/insulted: pass Resolve check or retaliate next action.", "recovery": "2D6 days"}, + {"key": "3.2", "name": "Guilty fixation", "lethal": false, "timeLimit": "", "effects": "-2 dice to WIT rolls unless pursuing obsession in scene.", "recovery": "2D6 days"}, + {"key": "3.3", "name": "Hallucinations (voices)", "lethal": false, "timeLimit": "", "effects": "Each shift: roll Insight; fail = lose next action to arguing/listening.", "recovery": "3D6 days"}, + {"key": "3.4", "name": "Flashbacks", "lethal": false, "timeLimit": "", "effects": "Blood/dim light: +1 Stress per round; cannot Push.", "recovery": "2D6 days"}, + {"key": "3.5", "name": "Fugue risk", "lethal": false, "timeLimit": "", "effects": "When Stress ≥5: once per scene GM may remove 1 action (lose time).", "recovery": "2D6 days"}, + {"key": "3.6", "name": "Self-medication", "lethal": false, "timeLimit": "", "effects": "If don't take coping substance daily, start next session at +2 Stress.", "recovery": "3D6 days"}, + + {"key": "4.1", "name": "Major breakdown", "lethal": false, "timeLimit": "", "effects": "Broken for D6 rounds; afterward -2 dice to mental rolls for 1 day.", "recovery": "D6 days"}, + {"key": "4.2", "name": "Catatonia", "lethal": false, "timeLimit": "", "effects": "Unresponsive for D6 rounds (combat) or 1 stretch (non-combat).", "recovery": "D6 days"}, + {"key": "4.3", "name": "Violent derealization", "lethal": false, "timeLimit": "", "effects": "In darkness allies count as 'unknown shapes': -2 dice to avoid friendly fire.", "recovery": "2D6 days"}, + {"key": "4.4", "name": "Panic flight", "lethal": false, "timeLimit": "", "effects": "If threatened: must Retreat unless pass Resolve; cannot hold position.", "recovery": "2D6 days"}, + {"key": "4.5", "name": "Ebb echo (if Ebon)", "lethal": false, "timeLimit": "", "effects": "Whenever you Push gain +1 extra Stress (so Push = +2 Stress total).", "recovery": "2D6 days"}, + {"key": "4.6", "name": "Camera-worthy mania", "lethal": false, "timeLimit": "", "effects": "-2 dice to Stealth; if don't do something bold each scene +1 Stress.", "recovery": "2D6 days"}, + + {"key": "5.1", "name": "Permanent phobia", "lethal": false, "timeLimit": "", "effects": "As Phobia but only therapy/arc resolution removes it.", "recovery": "Permanent until treated"}, + {"key": "5.2", "name": "Permanent paranoia", "lethal": false, "timeLimit": "", "effects": "Cannot accept Help dice without therapy/arc resolution.", "recovery": "Permanent until treated"}, + {"key": "5.3", "name": "Identity fracture", "lethal": false, "timeLimit": "", "effects": "Define new persona/compulsion; -1 die to EMP until resolved in play.", "recovery": "Permanent until resolved"}, + {"key": "5.4", "name": "Amnesia (episodic)", "lethal": false, "timeLimit": "", "effects": "Lose last shift of memory; GM introduces concrete complication/lead loss.", "recovery": "D6 days"}, + {"key": "5.5", "name": "Total amnesia", "lethal": false, "timeLimit": "", "effects": "Forget squad and role; must be managed in play.", "recovery": "D6 days or longer"}, + {"key": "5.6", "name": "Psychotic break", "lethal": false, "timeLimit": "", "effects": "Broken now; on recovery gain lasting delusion/compulsion.", "recovery": "Permanent until treated"}, + + {"key": "6.1", "name": "Heart seizure", "lethal": true, "timeLimit": "1 shift", "effects": "Each shift make Stamina; fail = death.", "recovery": "—"}, + {"key": "6.2", "name": "Suicide impulse", "lethal": true, "timeLimit": "1 shift", "effects": "If left alone attempt self-harm; allies can restrain/persuade/treat.", "recovery": "—"}, + {"key": "6.3", "name": "Total collapse", "lethal": true, "timeLimit": "1 day", "effects": "Stress cannot be relieved naturally; if Stress ≥7 at day end, die of shock.", "recovery": "—"}, + {"key": "6.4", "name": "Vegetative state", "lethal": true, "timeLimit": "1 day", "effects": "Unresponsive; only advanced care prevents permanent loss.", "recovery": "Permanent"}, + {"key": "6.5", "name": "Fatal panic", "lethal": true, "timeLimit": "1 shift", "effects": "Unless stabilized medically, you die.", "recovery": "—"}, + {"key": "6.6", "name": "Heart stops", "lethal": true, "timeLimit": "Instant", "effects": "You die immediately.", "recovery": "—"} + ] + }, + "rangedCriticalFail": { + "name": "Ranged Weapon Critical Fail (2-12)", + "description": "Trigger on pushed attack with stress bane and zero total successes.", + "formula": "2d6", + "results": [ + {"range": [2, 2], "text": "Awkward stance: You overextend. Suffer -1 die to your next ranged attack this scene.", "type": 0}, + {"range": [3, 3], "text": "Exposed: You lean too far from cover or step into the open. Enemies gain +1 die to attack you until the start of your next turn.", "type": 0}, + {"range": [4, 4], "text": "Fumbled reload: You mis-seat a magazine or drop a round. No damage, but you lose your next Fast action (or must spend a Fast action immediately) to fix it.", "type": 0}, + {"range": [5, 5], "text": "Slipped aim: Your shot goes wild. The GM may move your attack to a different target or object in the same zone.", "type": 0}, + {"range": [6, 6], "text": "Spent mag: You miscounted your rounds. After resolving the miss, your magazine is empty; set ammo to 0 for that magazine.", "type": 0}, + {"range": [7, 7], "text": "Jammed: The weapon jams hard. You cannot fire it again until you spend a Slow action to clear the jam.", "type": 0}, + {"range": [8, 8], "text": "Weapon dropped: Recoil or surprise makes you lose your grip. You drop the weapon in your zone; picking it up costs a Fast action.", "type": 0}, + {"range": [9, 9], "text": "Harsh recoil: You wrench your arm or shoulder. Take 1 damage ignoring armor and -1 die to all ranged attacks until you receive first aid or rest a stretch.", "type": 0}, + {"range": [10, 10], "text": "Collateral damage: Your stray fire hits something important. The GM creates an immediate new hazard that threatens you or your squad.", "type": 0}, + {"range": [11, 11], "text": "Catastrophic jam / partial break: A casing ruptures, or internal parts shear. Take 1 damage ignoring armor, and the weapon is broken until repaired.", "type": 0}, + {"range": [12, 12], "text": "Overload / explosion: The weapon catastrophically fails. Take 2 damage ignoring armor and suffer an automatic Physical Critical Injury roll. The weapon is destroyed.", "type": 0} + ] + }, + "meleeCriticalFail": { + "name": "Melee Weapon Critical Fail (2-12)", + "description": "Trigger on pushed attack with stress bane and zero total successes.", + "formula": "2d6", + "results": [ + {"range": [2, 2], "text": "Off balance: You over-swing. Suffer -1 die to your next Melee attack this scene.", "type": 0}, + {"range": [3, 3], "text": "Overextended: You stumble into a bad position. Enemies attacking you in Engaged range gain +1 die until the start of your next turn.", "type": 0}, + {"range": [4, 4], "text": "Slip or trip: You lose footing. You fall prone or stagger; standing costs a Fast action.", "type": 0}, + {"range": [5, 5], "text": "Minor strain: You tweak a wrist, elbow, or ankle. Take 1 damage ignoring armor but no other effect.", "type": 0}, + {"range": [6, 6], "text": "Friendly tangle: You clash with an ally’s position. One nearby ally suffers -1 die to their next action this round.", "type": 0}, + {"range": [7, 7], "text": "Weapon stuck / over-committed: Weapon user: weapon sticks; spend a Fast action to free it. Unarmed: cannot Parry until your next turn.", "type": 0}, + {"range": [8, 8], "text": "Weapon dropped / grip broken: Weapon user: drop weapon; pick up costs Fast action. Unarmed: suffer -2 dice to Melee until end of scene or treated.", "type": 0}, + {"range": [9, 9], "text": "Self-inflicted knock: You strike a solid surface. Take 1 damage ignoring armor and lose one Fast action on your next turn.", "type": 0}, + {"range": [10, 10], "text": "Bad landing / joint damage: Take 2 damage ignoring armor and suffer -2 dice to physical rolls involving that limb until healed.", "type": 0}, + {"range": [11, 11], "text": "Weapon break / serious strain: Weapon: damaged (-2 dice) until repaired; if already damaged, it breaks. Unarmed: take 2 damage ignoring armor and -2 dice to Melee until medical treatment.", "type": 0}, + {"range": [12, 12], "text": "Catastrophic self-harm: Take weapon’s base damage (or 2 unarmed) ignoring armor and immediately roll a Physical Critical Injury.", "type": 0} + ] + } +} diff --git a/packs/weapons.json b/packs/weapons.json new file mode 100644 index 0000000..7baffe4 --- /dev/null +++ b/packs/weapons.json @@ -0,0 +1,987 @@ +[ + { + "name": "BLA 046M Blitzer", + "type": "weapon", + "img": "systems/slazero2/assets/SLA_Assets/Weapons/BLA Blitzer.png", + "system": { + "description": "The BLA 046M is a heavy-framed revolver with a six-round cylinder. Reliable, powerful, and simple enough to maintain in the field. Favoured by veterans who don't trust magazines.", + "weaponType": "Revolver", + "category": "ranged", + "damage": 3, + "range": "Short", + "rof": 1, + "ap": 0, + "magazine": 6, + "ammo": 6, + "fireModes": [ + "single" + ], + "gearBonus": 0, + "autoAmmoUse": 0, + "ammoType": "standard", + "caliber": ".44", + "roundType": "standard", + "cost": 350, + "initiativeMod": 0, + "equipped": false, + "ammoEmpty": false + } + }, + { + "name": "CAF SMG", + "type": "weapon", + "img": "systems/slazero2/assets/SLA_Assets/Weapons/CAF SMG.png", + "system": { + "description": "CAF-issue submachine gun with top-feed magazine and folding stock. Favoured by patrol officers who need more firepower than a sidearm but less bulk than a rifle.", + "weaponType": "SMG", + "category": "ranged", + "damage": 2, + "range": "Short", + "rof": 3, + "ap": 0, + "magazine": 30, + "ammo": 30, + "fireModes": [ + "single", + "auto" + ], + "gearBonus": 0, + "autoAmmoUse": 8, + "ammoType": "standard", + "caliber": "10mm", + "roundType": "standard", + "cost": 500, + "initiativeMod": 0, + "equipped": false, + "ammoEmpty": false + } + }, + { + "name": "Chainaxe", + "type": "weapon", + "img": "systems/slazero2/assets/SLA_Assets/Weapons/Chainaxe1.png", + "system": { + "description": "A one-handed axe with a motorised chain running along the blade edge. The Chainaxe is louder than it needs to be. This is intentional. Favoured by Frothers and anyone who wants witnesses.", + "weaponType": "Axe", + "category": "melee", + "damage": 3, + "range": "Engaged", + "rof": 2, + "ap": 1, + "magazine": 0, + "ammo": 0, + "fireModes": [], + "gearBonus": 0, + "autoAmmoUse": 0, + "ammoType": "standard", + "caliber": "", + "roundType": "standard", + "cost": 460, + "initiativeMod": 0, + "equipped": false, + "ammoEmpty": false + } + }, + { + "name": "Chainsaw (Improvised)", + "type": "weapon", + "img": "systems/slazero2/assets/SLA_Assets/Weapons/Chainsaw1.png", + "system": { + "description": "An industrial power tool repurposed as a close-range weapon. Technically not a weapon. Technically. On a fumble, the operator takes 1d3 damage instead of the target.", + "weaponType": "Improvised", + "category": "melee", + "damage": 4, + "range": "Engaged", + "rof": 1, + "ap": 0, + "magazine": 0, + "ammo": 0, + "fireModes": [], + "gearBonus": 0, + "autoAmmoUse": 0, + "ammoType": "standard", + "caliber": "", + "roundType": "standard", + "cost": 120, + "initiativeMod": -1, + "equipped": false, + "ammoEmpty": false + } + }, + { + "name": "Ebb Sword", + "type": "weapon", + "img": "systems/slazero2/assets/SLA_Assets/Weapons/Ebb Infused Blade.png", + "system": { + "description": "A blade channelled with focused Ebb energy. The weapon strikes with physical force and Ebb disruption simultaneously. Only Ebon and Brain Waster operatives can maintain the Ebb channel required to use it at full effectiveness. Others can wield it but gain no Ebb bonus.", + "weaponType": "Blade", + "category": "melee", + "damage": 3, + "range": "Engaged", + "rof": 2, + "ap": 2, + "magazine": 0, + "ammo": 0, + "fireModes": [], + "gearBonus": 1, + "autoAmmoUse": 0, + "ammoType": "standard", + "caliber": "", + "roundType": "standard", + "cost": 1200, + "initiativeMod": 0, + "equipped": false, + "ammoEmpty": false + } + }, + { + "name": "FEN 10 Street Sweeper", + "type": "weapon", + "img": "systems/slazero2/assets/SLA_Assets/Weapons/FEN10 Street Sweeper.png", + "system": { + "description": "A drum-fed automatic shotgun. The Street Sweeper fires 12-gauge rounds from a 12-round drum at semi-automatic speed. At close range it is catastrophic. At any other range it is mostly theatrical.", + "weaponType": "Shotgun", + "category": "ranged", + "damage": 4, + "range": "Short", + "rof": 2, + "ap": 0, + "magazine": 12, + "ammo": 12, + "fireModes": [ + "single", + "burst" + ], + "gearBonus": 0, + "autoAmmoUse": 4, + "ammoType": "standard", + "caliber": "12g", + "roundType": "standard", + "cost": 900, + "initiativeMod": 0, + "equipped": false, + "ammoEmpty": false + } + }, + { + "name": "FEN 204 Heavy Pistol", + "type": "weapon", + "img": "systems/slazero2/assets/SLA_Assets/Weapons/FEN 204.png", + "system": { + "description": "The FEN 204 is a large-frame semi-automatic pistol with a ported compensator and underbarrel rail. Hits significantly harder than standard sidearms. The preferred sidearm of Dark Finders.", + "weaponType": "Pistol", + "category": "ranged", + "damage": 3, + "range": "Short", + "rof": 2, + "ap": 1, + "magazine": 12, + "ammo": 12, + "fireModes": [ + "single" + ], + "gearBonus": 1, + "autoAmmoUse": 0, + "ammoType": "standard", + "caliber": ".454", + "roundType": "standard", + "cost": 650, + "initiativeMod": 0, + "equipped": false, + "ammoEmpty": false + } + }, + { + "name": "FEN 209 Machine Pistol", + "type": "weapon", + "img": "systems/slazero2/assets/SLA_Assets/Weapons/FEN 209 Machine PIstol.png", + "system": { + "description": "A fully-automatic machine pistol. Fires 3mm rounds at high velocity from a compact frame. Devastating in close quarters; nearly useless beyond that. Favoured by operatives who need concealment over reach.", + "weaponType": "Machine Pistol", + "category": "ranged", + "damage": 2, + "range": "Short", + "rof": 4, + "ap": 0, + "magazine": 20, + "ammo": 20, + "fireModes": [ + "single", + "auto" + ], + "gearBonus": 0, + "autoAmmoUse": 8, + "ammoType": "standard", + "caliber": "3mm", + "roundType": "standard", + "cost": 380, + "initiativeMod": 0, + "equipped": false, + "ammoEmpty": false + } + }, + { + "name": "FEN 30-30 Rail Cannon", + "type": "weapon", + "img": "systems/slazero2/assets/SLA_Assets/Weapons/FEN 30-30.png", + "system": { + "description": "An electromagnetic rail-accelerated precision weapon. The 30-30 fires a tungsten slug at velocities that make most armour irrelevant. Requires a power pack per five shots. Extreme range, extreme penetration.", + "weaponType": "Rail Gun", + "category": "ranged", + "damage": 5, + "range": "Extreme", + "rof": 1, + "ap": 4, + "magazine": 5, + "ammo": 5, + "fireModes": [ + "single" + ], + "gearBonus": 2, + "autoAmmoUse": 0, + "ammoType": "standard", + "caliber": "Rail", + "roundType": "standard", + "cost": 4000, + "initiativeMod": -1, + "equipped": false, + "ammoEmpty": false + } + }, + { + "name": "FEN 401 Shiver Pistol", + "type": "weapon", + "img": "systems/slazero2/assets/SLA_Assets/Weapons/FEN 401 Shiver Pistol.png", + "system": { + "description": "Standard-issue sidearm for SLA law enforcement and Shivers. Reliable, accurate, and built to survive being dropped in a drain. Nothing special \u2014 which is exactly what CAF wanted.", + "weaponType": "Pistol", + "category": "ranged", + "damage": 2, + "range": "Short", + "rof": 2, + "ap": 0, + "magazine": 12, + "ammo": 12, + "fireModes": [ + "single" + ], + "gearBonus": 0, + "autoAmmoUse": 0, + "ammoType": "standard", + "caliber": "9mm", + "roundType": "standard", + "cost": 280, + "initiativeMod": 0, + "equipped": false, + "ammoEmpty": false + } + }, + { + "name": "FEN 603 Auto Pistol", + "type": "weapon", + "img": "systems/slazero2/assets/SLA_Assets/Weapons/FEN 603 Auto Pistol.png", + "system": { + "description": "An angular, high-capacity automatic pistol with an extended magazine. Fires in fully automatic bursts at close range. Less accurate than single-fire sidearms but devastating in a corridor.", + "weaponType": "Pistol", + "category": "ranged", + "damage": 2, + "range": "Short", + "rof": 3, + "ap": 0, + "magazine": 24, + "ammo": 24, + "fireModes": [ + "single", + "auto" + ], + "gearBonus": 0, + "autoAmmoUse": 8, + "ammoType": "standard", + "caliber": "9mm", + "roundType": "standard", + "cost": 420, + "initiativeMod": 0, + "equipped": false, + "ammoEmpty": false + } + }, + { + "name": "FEN 701 Urban Carbine", + "type": "weapon", + "img": "systems/slazero2/assets/SLA_Assets/Weapons/FEN701 Urban Carbine.png", + "system": { + "description": "A compact carbine designed for urban operations. Short enough for vehicle use, powerful enough for street-level engagements. Used extensively by SLA rapid response teams.", + "weaponType": "Carbine", + "category": "ranged", + "damage": 3, + "range": "Long", + "rof": 2, + "ap": 1, + "magazine": 24, + "ammo": 24, + "fireModes": [ + "single", + "burst" + ], + "gearBonus": 0, + "autoAmmoUse": 0, + "ammoType": "standard", + "caliber": "7mm", + "roundType": "standard", + "cost": 850, + "initiativeMod": 0, + "equipped": false, + "ammoEmpty": false + } + }, + { + "name": "FEN 706 Power Reaper", + "type": "weapon", + "img": "systems/slazero2/assets/SLA_Assets/Weapons/FEN 706.png", + "system": { + "description": "A rotary-barrel heavy weapon that fires high-velocity rounds at catastrophic volume. The Power Reaper is not a precision instrument \u2014 it is a statement. Requires a harness or mounting point to use effectively.", + "weaponType": "HW", + "category": "ranged", + "damage": 5, + "range": "Long", + "rof": 3, + "ap": 2, + "magazine": 100, + "ammo": 100, + "fireModes": [ + "auto" + ], + "gearBonus": 0, + "autoAmmoUse": 15, + "ammoType": "standard", + "caliber": "7.62mm", + "roundType": "standard", + "cost": 6000, + "initiativeMod": -2, + "equipped": false, + "ammoEmpty": false + } + }, + { + "name": "FEN 981 Sniper Rifle", + "type": "weapon", + "img": "systems/slazero2/assets/SLA_Assets/Weapons/FEN 981 Sniper Rifle.png", + "system": { + "description": "A bolt-action precision rifle with integrated suppressor, large magnification scope, and bipod. The FEN 981 is the weapon of choice for long-range elimination. Requires a prepared position and patience.", + "weaponType": "Sniper Rifle", + "category": "ranged", + "damage": 5, + "range": "Extreme", + "rof": 1, + "ap": 2, + "magazine": 10, + "ammo": 10, + "fireModes": [ + "single" + ], + "gearBonus": 2, + "autoAmmoUse": 0, + "ammoType": "standard", + "caliber": "8mm", + "roundType": "standard", + "cost": 2000, + "initiativeMod": -1, + "equipped": false, + "ammoEmpty": false + } + }, + { + "name": "FEN AR Assault Rifle", + "type": "weapon", + "img": "systems/slazero2/assets/SLA_Assets/Weapons/FEN AR Assault Rifle.png", + "system": { + "description": "The FEN AR is a reliable gas-operated assault rifle with a distinctive carry handle and three fire modes. A workhorse weapon used across SLA's operative divisions. Easy to maintain, hard to break.", + "weaponType": "Rifle", + "category": "ranged", + "damage": 3, + "range": "Long", + "rof": 3, + "ap": 1, + "magazine": 30, + "ammo": 30, + "fireModes": [ + "single", + "burst", + "auto" + ], + "gearBonus": 0, + "autoAmmoUse": 8, + "ammoType": "standard", + "caliber": "5.56mm", + "roundType": "standard", + "cost": 700, + "initiativeMod": 0, + "equipped": false, + "ammoEmpty": false + } + }, + { + "name": "FEN Reaper LMG", + "type": "weapon", + "img": "systems/slazero2/assets/SLA_Assets/Weapons/FEN Reaper LMG.png", + "system": { + "description": "A belt-fed light machine gun. The Reaper provides sustained suppressive fire. Fires until the belt runs out or the operator passes out \u2014 whichever comes first. Requires a bipod or resting position to use accurately.", + "weaponType": "LMG", + "category": "ranged", + "damage": 4, + "range": "Long", + "rof": 4, + "ap": 1, + "magazine": 100, + "ammo": 100, + "fireModes": [ + "auto" + ], + "gearBonus": 0, + "autoAmmoUse": 10, + "ammoType": "standard", + "caliber": "7mm", + "roundType": "standard", + "cost": 2200, + "initiativeMod": -1, + "equipped": false, + "ammoEmpty": false + } + }, + { + "name": "Fragmentation Grenade", + "type": "weapon", + "img": "systems/slazero2/assets/SLA_Assets/Weapons/Frag Grenade.png", + "system": { + "description": "A standard-pattern fragmentation grenade. On detonation, everyone within Short range takes DMG 3 (no armour dice, only auto). Between Short and Medium range takes DMG 1. Roll Mobility vs difficulty 2 to avoid the blast zone.", + "weaponType": "Grenade", + "category": "ranged", + "damage": 3, + "range": "Short", + "rof": 1, + "ap": 0, + "magazine": 1, + "ammo": 1, + "fireModes": [ + "single" + ], + "gearBonus": 0, + "autoAmmoUse": 0, + "ammoType": "standard", + "caliber": "40mm", + "roundType": "HE", + "cost": 80, + "initiativeMod": 0, + "equipped": false, + "ammoEmpty": false + } + }, + { + "name": "GA 50 Finisher", + "type": "weapon", + "img": "systems/slazero2/assets/SLA_Assets/Weapons/GA 50 Finisher.png", + "system": { + "description": "A heavy-frame semi-automatic pistol firing .50 calibre rounds. Slower than lighter sidearms but hits significantly harder. Standard sidearm for SLA special division operatives.", + "weaponType": "Pistol", + "category": "ranged", + "damage": 3, + "range": "Short", + "rof": 1, + "ap": 1, + "magazine": 8, + "ammo": 8, + "fireModes": [ + "single" + ], + "gearBonus": 1, + "autoAmmoUse": 0, + "ammoType": "standard", + "caliber": ".50", + "roundType": "standard", + "cost": 700, + "initiativeMod": 0, + "equipped": false, + "ammoEmpty": false + } + }, + { + "name": "GA 9442 Browbeater Rifle", + "type": "weapon", + "img": "systems/slazero2/assets/SLA_Assets/Weapons/GA 9442.png", + "system": { + "description": "A heavy battle rifle with a fluted barrel, integrated bipod, and large magnification scope. The Browbeater is built for long-range precision in sustained engagements. Not subtle. Extremely effective.", + "weaponType": "Rifle", + "category": "ranged", + "damage": 4, + "range": "Extreme", + "rof": 2, + "ap": 2, + "magazine": 20, + "ammo": 20, + "fireModes": [ + "single", + "burst" + ], + "gearBonus": 2, + "autoAmmoUse": 0, + "ammoType": "standard", + "caliber": "7.62mm", + "roundType": "standard", + "cost": 1400, + "initiativeMod": -1, + "equipped": false, + "ammoEmpty": false + } + }, + { + "name": "GA 9443 Mini-Browbeater", + "type": "weapon", + "img": "systems/slazero2/assets/SLA_Assets/Weapons/GA 9443.png", + "system": { + "description": "A bullpup-configured compact variant of the Browbeater. The magazine sits behind the pistol grip, keeping overall length short while retaining most of the parent rifle's hitting power. Popular with vehicle crews and scouts.", + "weaponType": "Rifle", + "category": "ranged", + "damage": 3, + "range": "Long", + "rof": 2, + "ap": 1, + "magazine": 20, + "ammo": 20, + "fireModes": [ + "single", + "burst" + ], + "gearBonus": 1, + "autoAmmoUse": 0, + "ammoType": "standard", + "caliber": "7.62mm", + "roundType": "standard", + "cost": 950, + "initiativeMod": 0, + "equipped": false, + "ammoEmpty": false + } + }, + { + "name": "Gash Fist", + "type": "weapon", + "img": "systems/slazero2/assets/SLA_Assets/Weapons/GASH First.png", + "system": { + "description": "A spiked knuckle weapon with four reinforced blades extending from a metal plate. Worn over the fist. Brutal in close quarters, and leaves wounds that are difficult to stitch. Favoured by Frothers and Carrien-hunters.", + "weaponType": "Natural", + "category": "melee", + "damage": 3, + "range": "Engaged", + "rof": 2, + "ap": 1, + "magazine": 0, + "ammo": 0, + "fireModes": [], + "gearBonus": 0, + "autoAmmoUse": 0, + "ammoType": "standard", + "caliber": "", + "roundType": "standard", + "cost": 180, + "initiativeMod": 0, + "equipped": false, + "ammoEmpty": false + } + }, + { + "name": "ITB Mutilator Sword", + "type": "weapon", + "img": "systems/slazero2/assets/SLA_Assets/Weapons/Chain Knife.png", + "system": { + "description": "A two-handed chain-edged claymore produced by ITB. The motorised chain teeth run continuously along both edges. It does not cut \u2014 it tears. Wounds from a Mutilator are extremely difficult to stabilise.", + "weaponType": "Blade", + "category": "melee", + "damage": 4, + "range": "Engaged", + "rof": 1, + "ap": 2, + "magazine": 0, + "ammo": 0, + "fireModes": [], + "gearBonus": 1, + "autoAmmoUse": 0, + "ammoType": "standard", + "caliber": "", + "roundType": "standard", + "cost": 850, + "initiativeMod": -1, + "equipped": false, + "ammoEmpty": false + } + }, + { + "name": "KPS Combat Shotgun", + "type": "weapon", + "img": "systems/slazero2/assets/SLA_Assets/Weapons/KPS Shotgun.png", + "system": { + "description": "A pump-action double-barrel combat shotgun. Each pump chambers two shells simultaneously. Devastating at close range and capable of knocking targets off their feet. Not a weapon for long corridors.", + "weaponType": "Shotgun", + "category": "ranged", + "damage": 4, + "range": "Short", + "rof": 1, + "ap": 0, + "magazine": 8, + "ammo": 8, + "fireModes": [ + "single" + ], + "gearBonus": 0, + "autoAmmoUse": 0, + "ammoType": "standard", + "caliber": "12g", + "roundType": "standard", + "cost": 550, + "initiativeMod": 0, + "equipped": false, + "ammoEmpty": false + } + }, + { + "name": "MAC Knife", + "type": "weapon", + "img": "systems/slazero2/assets/SLA_Assets/Weapons/MAC Knife.png", + "system": { + "description": "A recurve combat knife with a distinctive swept quillon guard. The MAC blade is heavier than a standard knife and designed for penetrating body armour through gaps and joints. Issue blade for several SLA divisions.", + "weaponType": "Knife", + "category": "melee", + "damage": 2, + "range": "Engaged", + "rof": 2, + "ap": 1, + "magazine": 0, + "ammo": 0, + "fireModes": [], + "gearBonus": 0, + "autoAmmoUse": 0, + "ammoType": "standard", + "caliber": "", + "roundType": "standard", + "cost": 120, + "initiativeMod": 0, + "equipped": false, + "ammoEmpty": false + } + }, + { + "name": "MILA Grenade Launcher", + "type": "weapon", + "img": "systems/slazero2/assets/SLA_Assets/Weapons/MILA Grenage Launcher.png", + "system": { + "description": "A six-round revolving cylinder grenade launcher. Fires 40mm fragmentation, smoke, or stun grenades. The MILA is a crowd-control and area-denial weapon. Anything within its blast radius disagrees with the concept of cover.", + "weaponType": "Launcher", + "category": "ranged", + "damage": 5, + "range": "Long", + "rof": 1, + "ap": 1, + "magazine": 6, + "ammo": 6, + "fireModes": [ + "single" + ], + "gearBonus": 0, + "autoAmmoUse": 0, + "ammoType": "standard", + "caliber": "40mm", + "roundType": "HE", + "cost": 1800, + "initiativeMod": -1, + "equipped": false, + "ammoEmpty": false + } + }, + { + "name": "Pacifier Baton", + "type": "weapon", + "img": "systems/slazero2/assets/SLA_Assets/Weapons/Pacifier Baton.png", + "system": { + "description": "A T-handled riot control baton with a built-in electroshock tip. Used by Shivers and crowd-control units. On a successful hit, the target must pass a Stamina roll or gain the Stunned condition.", + "weaponType": "Baton", + "category": "melee", + "damage": 2, + "range": "Engaged", + "rof": 2, + "ap": 0, + "magazine": 0, + "ammo": 0, + "fireModes": [], + "gearBonus": 0, + "autoAmmoUse": 0, + "ammoType": "standard", + "caliber": "", + "roundType": "standard", + "cost": 90, + "initiativeMod": 0, + "equipped": false, + "ammoEmpty": false + } + }, + { + "name": "Power Claymore", + "type": "weapon", + "img": "systems/slazero2/assets/SLA_Assets/Weapons/Powerclaymore.png", + "system": { + "description": "A two-handed power-assisted claymore with a wide double-edged blade and integrated power cell in the guard. Capable of cleaving through armour plating. Requires two hands and the will to use it.", + "weaponType": "Blade", + "category": "melee", + "damage": 4, + "range": "Engaged", + "rof": 1, + "ap": 2, + "magazine": 0, + "ammo": 0, + "fireModes": [], + "gearBonus": 0, + "autoAmmoUse": 0, + "ammoType": "standard", + "caliber": "", + "roundType": "standard", + "cost": 700, + "initiativeMod": -1, + "equipped": false, + "ammoEmpty": false + } + }, + { + "name": "Riot Shield", + "type": "weapon", + "img": "systems/slazero2/assets/SLA_Assets/Armor/2nd try/Riot Shield.png", + "system": { + "description": "A full-body ballistic shield used by Shivers and riot control units. While carried, the user gains +1 automatic armour against ranged attacks from the front. Can be used to bash for 1d4 damage.", + "weaponType": "Shield", + "category": "melee", + "damage": 2, + "range": "Engaged", + "rof": 1, + "ap": 0, + "magazine": 0, + "ammo": 0, + "fireModes": [], + "gearBonus": 0, + "autoAmmoUse": 0, + "ammoType": "standard", + "caliber": "", + "roundType": "standard", + "cost": 300, + "initiativeMod": 0, + "equipped": false, + "ammoEmpty": false + } + }, + { + "name": "SLA Blade", + "type": "weapon", + "img": "systems/slazero2/assets/SLA_Assets/Weapons/SLA Blade.png", + "system": { + "description": "Standard single-handed combat blade issued across SLA operative divisions. A straight, single-edged weapon optimised for quick thrusting attacks. Reliable, balanced, and unremarkable \u2014 exactly as intended.", + "weaponType": "Blade", + "category": "melee", + "damage": 3, + "range": "Engaged", + "rof": 2, + "ap": 1, + "magazine": 0, + "ammo": 0, + "fireModes": [], + "gearBonus": 0, + "autoAmmoUse": 0, + "ammoType": "standard", + "caliber": "", + "roundType": "standard", + "cost": 280, + "initiativeMod": 0, + "equipped": false, + "ammoEmpty": false + } + }, + { + "name": "Shaktar Claws", + "type": "weapon", + "img": "systems/slazero2/assets/icons/weapons/weapon-shaktar-claws.svg", + "system": { + "description": "The retractable battle-claws of a Shaktar. Hard as ceramic, honed by martial tradition. Shaktar do not draw them without intent to wound. Fast precise strikes aimed at vital points.", + "weaponType": "Natural", + "category": "melee", + "damage": 2, + "range": "Engaged", + "rof": 2, + "ap": 1, + "magazine": 0, + "ammo": 0, + "fireModes": [], + "gearBonus": 0, + "autoAmmoUse": 0, + "ammoType": "standard", + "caliber": "", + "roundType": "standard", + "cost": 0, + "initiativeMod": 0, + "equipped": true, + "ammoEmpty": false + } + }, + { + "name": "Stormer Chuckleduster", + "type": "weapon", + "img": "systems/slazero2/assets/SLA_Assets/Weapons/Spike Gauntlet.png", + "system": { + "description": "A reinforced brass-knuckle weapon scaled for Stormer-class physiques. The mass and density of the weapon combined with a Stormer's natural strength make it capable of structural damage. The name was coined by the first squad to use one.", + "weaponType": "Natural", + "category": "melee", + "damage": 4, + "range": "Engaged", + "rof": 2, + "ap": 0, + "magazine": 0, + "ammo": 0, + "fireModes": [], + "gearBonus": 0, + "autoAmmoUse": 0, + "ammoType": "standard", + "caliber": "", + "roundType": "standard", + "cost": 200, + "initiativeMod": 0, + "equipped": false, + "ammoEmpty": false + } + }, + { + "name": "Stormer Claws", + "type": "weapon", + "img": "systems/slazero2/assets/icons/weapons/weapon-stormer-claws.svg", + "system": { + "description": "Hardened bio-engineered talons grown into the hands of a 313-S Stormer. They can punch through vehicle panels and are near-impossible to disarm. A Stormer who fights with their claws fights without restraint.", + "weaponType": "Natural", + "category": "melee", + "damage": 3, + "range": "Engaged", + "rof": 2, + "ap": 1, + "magazine": 0, + "ammo": 0, + "fireModes": [], + "gearBonus": 0, + "autoAmmoUse": 0, + "ammoType": "standard", + "caliber": "", + "roundType": "standard", + "cost": 0, + "initiativeMod": 0, + "equipped": true, + "ammoEmpty": false + } + }, + { + "name": "Stormer Teeth", + "type": "weapon", + "img": "systems/slazero2/assets/icons/weapons/weapon-stormer-teeth.svg", + "system": { + "description": "The bite of a 313-S Stormer can shear through light armour plating. The jaw muscles of a Stormer are engineered for combat and can exert forces that break bone. Reserved for close-quarters desperation or deliberate brutality.", + "weaponType": "Natural", + "category": "melee", + "damage": 3, + "range": "Engaged", + "rof": 1, + "ap": 1, + "magazine": 0, + "ammo": 0, + "fireModes": [], + "gearBonus": 0, + "autoAmmoUse": 0, + "ammoType": "standard", + "caliber": "", + "roundType": "standard", + "cost": 0, + "initiativeMod": 0, + "equipped": true, + "ammoEmpty": false + } + }, + { + "name": "Stun Grenade", + "type": "weapon", + "img": "systems/slazero2/assets/SLA_Assets/Gear/FlashBang .png", + "system": { + "description": "A flashbang/stun grenade. On detonation, all targets within Short range must pass a Stamina roll (difficulty 2) or gain the Stunned condition (-3 dice to all rolls until end of next turn). No physical damage.", + "weaponType": "Grenade", + "category": "ranged", + "damage": 0, + "range": "Short", + "rof": 1, + "ap": 0, + "magazine": 1, + "ammo": 1, + "fireModes": [ + "single" + ], + "gearBonus": 0, + "autoAmmoUse": 0, + "ammoType": "standard", + "caliber": "40mm", + "roundType": "standard", + "cost": 60, + "initiativeMod": 0, + "equipped": false, + "ammoEmpty": false + } + }, + { + "name": "Vevaphon Claws", + "type": "weapon", + "img": "systems/slazero2/assets/icons/weapons/weapon-vevaphon-claws.svg", + "system": { + "description": "The adaptive natural weapons of a Stormer Vevaphon. Even in base form these claws carry cutting force beyond a human hand. In morph form they shift and harden further \u2014 Brute, Stalker and Raptor forms each intensify their lethality. The claws remember what the body becomes.", + "weaponType": "Natural", + "category": "melee", + "damage": 2, + "range": "Engaged", + "rof": 2, + "ap": 1, + "magazine": 0, + "ammo": 0, + "fireModes": [], + "gearBonus": 0, + "autoAmmoUse": 0, + "ammoType": "standard", + "caliber": "", + "roundType": "standard", + "cost": 0, + "initiativeMod": 0, + "equipped": true, + "ammoEmpty": false + } + }, + { + "name": "Vibro Sabre", + "type": "weapon", + "img": "systems/slazero2/assets/SLA_Assets/Weapons/Vibro Sabre.png", + "system": { + "description": "A curved sabre with a high-frequency vibration spine that disrupts molecular cohesion on contact. The Vibro Sabre cuts through armour as easily as flesh. The whine it produces before a strike is unmistakable \u2014 and terrifying.", + "weaponType": "Blade", + "category": "melee", + "damage": 3, + "range": "Engaged", + "rof": 2, + "ap": 3, + "magazine": 0, + "ammo": 0, + "fireModes": [], + "gearBonus": 0, + "autoAmmoUse": 0, + "ammoType": "standard", + "caliber": "", + "roundType": "standard", + "cost": 900, + "initiativeMod": 0, + "equipped": false, + "ammoEmpty": false + } + } +] \ No newline at end of file diff --git a/slazero2.mjs b/slazero2.mjs new file mode 100644 index 0000000..3dd6740 --- /dev/null +++ b/slazero2.mjs @@ -0,0 +1,10362 @@ +// SLA Zero Two System +console.log('SLA Zero Two | Loading System'); + +const ZERO_ENGINE_CORE_COMBAT = { + combatClass: CONFIG?.Combat?.documentClass ?? null, + combatantClass: CONFIG?.Combatant?.documentClass ?? null, + combatTrackerClass: CONFIG?.ui?.combat ?? null, + captured: false +}; + +function restoreZeroEngineCoreCombatConfig() { + const { combatClass, combatantClass, combatTrackerClass } = ZERO_ENGINE_CORE_COMBAT; + if (combatClass) CONFIG.Combat.documentClass = combatClass; + if (combatantClass) CONFIG.Combatant.documentClass = combatantClass; + if (combatTrackerClass) CONFIG.ui.combat = combatTrackerClass; +} + +function ensureCombatantCompatibilityMethods() { + const applyCompat = (proto) => { + if (!proto) return; + if (typeof proto.getSpeedFromActor !== "function") { + proto.getSpeedFromActor = function() { return 1; }; + } + if (typeof proto.getColor !== "function") { + proto.getColor = function() { return "#efefef"; }; + } + }; + + applyCompat(CONFIG?.Combatant?.documentClass?.prototype); + applyCompat(globalThis.Combatant?.prototype); + applyCompat(foundry?.documents?.Combatant?.prototype); +} + +/** + * Year SLA Zero Two Dice Dialog + * Allows modifying dice pool before rolling + */ +class YZEDiceDialog extends Dialog { + constructor(baseDice, label, dialogData = {}, options = {}) { + super(dialogData, options); + this.baseDice = baseDice; + this.label = label; + } + + static async show(baseDice, label, formula = '', options = {}) { + return new Promise((resolve) => { + + // ── Showing Off counter (shared between render + roll callback) ────────── + let showingOff = 0; + + const normalizedFireModes = Array.isArray(options.fireModes) + ? options.fireModes + : (typeof options.fireModes === "string" + ? options.fireModes.split(",").map(s => s.trim()).filter(Boolean) + : []); + const showFireMode = normalizedFireModes.length > 0; + const showAmmoType = Array.isArray(options.ammoTypes) && options.ammoTypes.length > 0; + const defaultFireMode = options.defaultFireMode || (showFireMode ? normalizedFireModes[0] : "single"); + const defaultAmmoType = options.defaultAmmoType || (showAmmoType ? options.ammoTypes[0] : "standard"); + const modeBonusMap = options.modeBonusMap || {}; + const ammoBonusMap = options.ammoBonusMap || {}; + const showAttackMods = !!options.showAttackMods; + const shotOptions = Array.isArray(options.shotOptions) ? options.shotOptions : []; + const showShotSelector = shotOptions.length > 1; + + const dialog = new Dialog({ + title: `Roll ${label}`, + content: ` +
+ ${formula ? `
+ +
+ ${formula} +
+
` : ''} +
+ +
+ ${baseDice}d6 +
+
+
+ +
+ + + + + +
+
+
+ +
+ ${baseDice}d6 +
+
+ +
+ +
+
+ + ${showShotSelector ? ` +
+ + +
+ ` : ''} + ${showFireMode ? ` +
+ + +
+ ` : ''} + ${showAmmoType ? ` +
+ + +
+ ` : ''} + ${showAttackMods ? ` +
+ + +
+
+ +
+
+ +
+ ` : ''} +
+ `, + buttons: { + roll: { + icon: '', + label: "Roll", + callback: (html) => { + const modifier = parseInt(html.find('[name="modifier"]').val()) || 0; + const fireMode = html.find('[name="fireMode"]').val() || defaultFireMode; + const ammoType = html.find('[name="ammoType"]').val() || defaultAmmoType; + const rangeBand = html.find('[name="rangeBand"]').val() || "normal"; + const useScope = html.find('[name="useScope"]').is(':checked'); + const useAI = html.find('[name="useAI"]').is(':checked'); + const modeBonus = parseInt(modeBonusMap[fireMode]) || 0; + const ammoBonus = parseInt(ammoBonusMap[ammoType]) || 0; + const rangeBonus = rangeBand === "point_blank" ? 1 : (rangeBand === "extreme" ? -2 : 0); + const scopeBonus = (useScope && (rangeBand === "long" || rangeBand === "extreme")) ? 2 : 0; + const aiBonus = useAI ? 2 : 0; + const shotIdx = parseInt(html.find('[name="shotCount"]').val()) || 0; + const chosenShot = shotOptions[shotIdx] || shotOptions[0] || { shots: 1, diceBonus: 0 }; + const shotDiceBonus = chosenShot.diceBonus || 0; + const shotsChosen = chosenShot.shots || 1; + // Showing off reduces pool — minimum 1d6 + const totalDice = Math.max(1, baseDice + modifier + modeBonus + ammoBonus + rangeBonus + scopeBonus + aiBonus + shotDiceBonus - showingOff); + if (options.returnDetails) { + resolve({ totalDice, fireMode, ammoType, modifier, rangeBand, useScope, useAI, rangeBonus, scopeBonus, aiBonus, shotsChosen, shotDiceBonus, showingOff }); + } else { + resolve(totalDice); + } + } + }, + cancel: { + icon: '', + label: "Cancel", + callback: () => resolve(null) + } + }, + default: "roll", + render: (html) => { + const modifierInput = html.find('[name="modifier"]'); + const totalDisplay = html.find('#total-dice'); + const soDisplay = html.find('#so-display'); + + const updateTotal = () => { + const modifier = parseInt(modifierInput.val()) || 0; + const fireMode = html.find('[name="fireMode"]').val() || defaultFireMode; + const ammoType = html.find('[name="ammoType"]').val() || defaultAmmoType; + const rangeBand = html.find('[name="rangeBand"]').val() || "normal"; + const useScope = html.find('[name="useScope"]').is(':checked'); + const useAI = html.find('[name="useAI"]').is(':checked'); + const modeBonus = parseInt(modeBonusMap[fireMode]) || 0; + const ammoBonus = parseInt(ammoBonusMap[ammoType]) || 0; + const rangeBonus = rangeBand === "point_blank" ? 1 : (rangeBand === "extreme" ? -2 : 0); + const scopeBonus = (useScope && (rangeBand === "long" || rangeBand === "extreme")) ? 2 : 0; + const aiBonus = useAI ? 2 : 0; + const total = Math.max(1, baseDice + modifier + modeBonus + ammoBonus + rangeBonus + scopeBonus + aiBonus - showingOff); + totalDisplay.text(`${total}d6`); + if (showingOff > 0) { + totalDisplay.css('color', '#ffaa00'); + soDisplay.html(`✨ Showing Off: ${showingOff} × (−${showingOff} dice)`); + } + }; + + modifierInput.on('input', updateTotal); + html.find('[name="fireMode"]').on('change', updateTotal); + html.find('[name="ammoType"]').on('change', updateTotal); + html.find('[name="rangeBand"]').on('change', updateTotal); + html.find('[name="useScope"]').on('change', updateTotal); + html.find('[name="useAI"]').on('change', updateTotal); + + html.find('.modify-dice').on('click', (event) => { + const modifier = parseInt(event.currentTarget.dataset.modifier); + const currentValue = parseInt(modifierInput.val()) || 0; + modifierInput.val(currentValue + modifier); + updateTotal(); + }); + + // ── SHOWING OFF button ───────────────────────────────────────────── + html.find('#showing-off-btn').on('click', () => { + // Check current pool would stay >= 1 + const modifier = parseInt(modifierInput.val()) || 0; + const rangeBonus = (() => { + const rb = html.find('[name="rangeBand"]').val() || "normal"; + return rb === "point_blank" ? 1 : (rb === "extreme" ? -2 : 0); + })(); + const currentTotal = baseDice + modifier + rangeBonus - showingOff; + if (currentTotal <= 1) { + ui.notifications.warn("Can't show off any more — pool is already at 1d6 minimum!"); + return; + } + + showingOff++; + updateTotal(); + _playShowingOffFanfare(); + + // Async side-effects — update actor tally + chat message + (async () => { + const actorId = options.actorId; + const actorName = options.actorName ?? 'Someone'; + let newTally = showingOff; // fallback if no actor + + if (actorId) { + const actor = game.actors.get(actorId); + if (actor) { + const prev = Number(actor.system.details?.showingOff ?? 0); + newTally = prev + 1; + await actor.update({ 'system.details.showingOff': newTally }); + } + } + + const exclamations = ['!', ' AGAIN!', ' EVEN MORE!', ' OUTRAGEOUSLY!', ' SPECTACULARLY!', ' BRAZENLY!']; + const excl = exclamations[Math.min(showingOff - 1, exclamations.length - 1)]; + const starBar = '✨'.repeat(Math.min(showingOff, 5)); + + await ChatMessage.create({ + speaker: { alias: actorName }, + content: ` +
+
+ ${starBar} SHOWING OFF${excl} ${starBar} +
+
+ ${actorName} is playing to the cameras! +
+
+ This roll: −${showingOff} dice  |  Career total: ${newTally} pt${newTally !== 1 ? 's' : ''} +
+
` + }); + + // 20-point milestone — broadcast publicly + if (actorId && newTally === 20) { + await ChatMessage.create({ + speaker: { alias: '⚠ SLA Zero Two' }, + content: ` +
+
+ ⚠ 20 POINTS OF SHOWING OFF ⚠ +
+
+ ${actorName} has reached 20 points of showing off.
+ GM — time to make them pay for it. +
+
` + }); + } + })(); + }); + } + }, { + width: 420, + classes: ["slazero2", "dialog"] + }); + + dialog.render(true); + }); + } +} + +/** + * SLA Zero Two Item Sheet + */ +class ZeroEngineItemSheet extends foundry.appv1.sheets.ItemSheet { + /** @override */ + static get defaultOptions() { + return foundry.utils.mergeObject(super.defaultOptions, { + classes: ["slazero2", "sheet", "item"], + width: 520, + height: 480, + tabs: [] + }); + } + + /** @override */ + get template() { + return `systems/slazero2/templates/item/item-sheet.hbs`; + } + + /** @override */ + getData() { + const context = super.getData(); + const itemData = this.item.toObject(false); + context.system = itemData.system; + context.flags = itemData.flags; + context.editable = this.isEditable; + return context; + } +} + +/** + * SLA Zero Two Actor Sheet + */ +class ZeroEngineActorSheet extends foundry.appv1.sheets.ActorSheet { + /** @override */ + static get defaultOptions() { + return foundry.utils.mergeObject(super.defaultOptions, { + classes: ["slazero2", "sheet", "actor"], + width: 1020, + height: 800, + tabs: [{ navSelector: ".sheet-tabs", contentSelector: ".sheet-body", initial: "main" }] + }); + } + + /** @override */ + get template() { + if (this.actor.type === "character") { + return `systems/slazero2/templates/actor/character-sheet.hbs`; + } + return `systems/slazero2/templates/actor/${this.actor.type}-sheet.hbs`; + } + + /** @override */ + getData() { + const context = super.getData(); + const actorData = this.actor.toObject(false); + // Use the live prepared system data so that programmatic updates (e.g. stress + // increments from a push roll) are reflected immediately on re-render. + // toObject(false) returns persisted source data which can lag behind actor.system + // in Foundry v14's optimistic-update model. + context.system = this.actor.system; + context.flags = actorData.flags; + + // Add rollData for convenience + context.rollData = context.actor.getRollData(); + + // Gather owned specialty items for drag-and-drop display + context.specialties = this.actor.items.filter(i => i.type === "specialty").map(i => i.toObject(false)); + + // Gather owned weapon items, split by category + // A weapon is melee if: category === "melee" OR range is "Engaged" (YZE melee range indicator) + const weapons = this.actor.items.filter(i => i.type === "weapon").map(i => i.toObject(false)); + const isMeleeWeapon = w => w.system.category === "melee" || + String(w.system.range ?? "").toLowerCase() === "engaged"; + context.meleeWeapons = weapons.filter(w => isMeleeWeapon(w)); + context.rangedWeapons = weapons.filter(w => !isMeleeWeapon(w)); + context.armors = this.actor.items.filter(i => i.type === "armor").map(i => i.toObject(false)); + // Expose equipped armor name for header display + context.equippedArmorName = context.armors[0]?.name || null; + + // Finances summary (used by both Mk1 and Mk2 sheets) + context.isGM = game.user?.isGM ?? false; + const ladCost = (this.actor.system.details?.ladAccount === true) ? 50 : 0; + if (!this.actor.system.finances) { + context.weeklyIncome = 0; context.weeklyExpenses = 0; context.weeklyNet = 0; + context.weeklyNetSign = '+'; context.weeklyNetColor = '#44cc66'; + } else { + const _inc = this.actor.system.finances?.income || {}; + const _exp = this.actor.system.finances?.expenses || {}; + context.weeklyIncome = (_inc.salary||0) + (_inc.bpnReward||0) + (_inc.other||0); + context.weeklyExpenses = (_exp.accommodation||0) + (_exp.drugs||0) + (_exp.subscriptions||0) + (_exp.other||0) + (_exp.bulletTax||0) + ladCost; + context.weeklyNet = context.weeklyIncome - context.weeklyExpenses; + context.weeklyNetSign = context.weeklyNet >= 0 ? '+' : ''; + context.weeklyNetColor = context.weeklyNet >= 0 ? '#44cc66' : '#cc1111'; + } + context.gearItems = this.actor.items + .filter(i => i.type === "equipment" && i.system?.isDrug !== true) + .map(i => i.toObject(false)); + context.drugs = this.actor.items + .filter(i => i.type === "equipment" && i.system?.isDrug === true) + .map(i => i.toObject(false)); + context.drugAlerts = this._buildDrugAlerts(this.actor); + + // EBB user detection — Ebon and Brain Waster are ebb users + const race = String(this.actor.system.race || "").toLowerCase(); + // Flat string versions for dropdown selected-state matching in templates + context.raceStr = race; + const trainingRaw = this.actor.system.training; + context.trainingStr = String(Array.isArray(trainingRaw) ? (trainingRaw[0] || "") : (trainingRaw || "")).toLowerCase(); + context.isEbbUser = (race === "ebon" || race === "brainwaster"); + context.isBrainWaster = (race === "brainwaster"); + context.isVevaphon = (race === "vevaphon"); + if (context.isVevaphon) { + const instab = Number(context.system.vevaphon?.instability ?? 0); + context.instabilityPips = Array.from({ length: 12 }, (_, i) => ({ + filled: i < instab, + danger: i >= 9 + })); + } + context.ebbFormulae = this.actor.items.filter(i => i.type === "ebb").map(i => i.toObject(false)); + // Expose archetype label for Mk2 sheet header + context.archetype = this.actor.system.archetype || (this.actor.system.training?.[0] ?? ""); + // Flux max = Empathy attribute value (core YZE rule: ebb pool scales with Empathy) + // Flux max = Ebb skill + 1 for Ebb users. Fall back to stored flux.max, then Empathy. + const _ebbSkillForFlux = parseInt(this.actor.system.skills?.ebb?.value) || 0; + const _storedFluxMax = parseInt(this.actor.system.flux?.max) || 0; + context.computedFluxMax = _ebbSkillForFlux > 0 + ? _ebbSkillForFlux + 1 + : (_storedFluxMax > 0 ? _storedFluxMax : (parseInt(this.actor.system.attributes?.empathy?.value) || 2)); + + // Add config object for template + context.config = { + natures: { + "human": "Human", + "replicant": "Replicant" + }, + archetypes: { + "operative": "Operative", + "investigator": "Investigator", + "scout": "Scout", + "medic": "Medic", + "tech": "Tech" + }, + Icons: { + tabs: { + stats: '', + mods: '', + combat: '', + inventory: '', + bio: '' + } + } + }; + + context.stressMax = 10; + + // Active SLA conditions for display on sheet + const SLA_COND_LABELS = { + 'sla-bleeding': { label: 'Bleeding', description: 'Lose 1 HP per round', icon: 'fas fa-tint' }, + 'sla-stunned': { label: 'Stunned', description: '-3 dice all rolls', icon: 'fas fa-dizzy' }, + 'sla-pinned': { label: 'Pinned', description: '-2 dice attacks/mobility', icon: 'fas fa-anchor' }, + 'sla-suppressed':{ label: 'Suppressed', description: '-2 dice all actions', icon: 'fas fa-shield-alt' }, + 'sla-on-fire': { label: 'On Fire', description: 'HP drain per turn, escalating', icon: 'fas fa-fire' }, + 'sla-exhausted': { label: 'Exhausted', description: '-2 dice all actions', icon: 'fas fa-bed' }, + 'sla-broken': { label: 'Broken', description: 'HP at 0 — critical state', icon: 'fas fa-heart-broken' }, + 'sla-winded': { label: 'Winded', description: '-1 die all rolls this round', icon: 'fas fa-wind' }, + 'sla-broken-arm':{ label: 'Broken Arm', description: '-2 Melee, -1 Marks/Force', icon: 'fas fa-bone' }, + 'sla-broken-leg':{ label: 'Broken Leg', description: '-2 Mobility, -1 Stealth', icon: 'fas fa-bone' }, + 'sla-concussed': { label: 'Concussed', description: '-1 all, extra -1 WIT rolls', icon: 'fas fa-brain' }, + 'sla-gut-wound': { label: 'Gut Wound', description: '-2 dice all rolls', icon: 'fas fa-user-injured' }, + 'sla-panicking': { label: 'Panicking', description: '-2 dice all rolls', icon: 'fas fa-skull' }, + 'sla-shaken': { label: 'Shaken', description: '-1 die all rolls', icon: 'fas fa-ghost' }, + }; + const statuses = this.actor.statuses ?? new Set(); + context.activeConditions = [...statuses] + .filter(id => SLA_COND_LABELS[id]) + .map(id => ({ id, ...SLA_COND_LABELS[id] })); + + // Drug withdrawal flag for sheet display + const hasDrugDep = this.actor.items.some( + i => i.type === 'specialty' && (i.name === 'Drug Dependency' || i.system?.drugDependent === true) + ); + const anyDrugActive = this.actor.items.some( + i => (i.system?.isDrug || i.type === 'drug' || i.system?.drugDependent === false) && i.system?.active === true + ); + context.drugWithdrawal = hasDrugDep && !anyDrugActive; + + return context; + } + + /** @override */ + activateListeners(html) { + html.find(".init-agi").click(ev => { + ev.preventDefault(); + this._rollInitiative(true); + }); + html.find(".init-wit").click(ev => { + ev.preventDefault(); + this._rollInitiative(false); + }); + + super.activateListeners(html); + + // Mirror inputs: inputs with data-mirrors="field" proxy their value to the canonical + // hidden input (which has the name attr) so only one binding ever reaches the form. + html.find('[data-mirrors]').on('input change', function() { + const target = $(this).data('mirrors'); + const canonical = html.find(`[name="${target}"]`); + if (canonical.length) { + canonical.val(this.value); + canonical.trigger('change'); + } + }); + + // Everything below here is only needed if the sheet is editable + if (!this.isEditable) return; + + // Add click handlers for rollable items + html.find('.rollable').click(this._onRoll.bind(this)); + + // Attribute increment/decrement buttons (new template) + html.find('.attr-btn').click(this._onAttributeChange.bind(this)); + + // Attribute roll buttons — both the header button and the die badge + html.find('.attr-roll-btn, .ze-attr-roll-btn').click(this._onAttributeRoll.bind(this)); + + // Skill increment/decrement buttons (new template) + html.find('.skill-increment, .skill-decrement').click(this._onSkillChange.bind(this)); + + // Skill roll — clicking the name link OR the existing .skill-roll elements + html.find('.skill-roll, .ze-skill-name').click(this._onSkillRoll.bind(this)); + + // Critical Injury roll buttons + html.find('.roll-physical-crit-btn').click(this._onRollPhysicalCrit.bind(this)); + html.find('.roll-mental-crit-btn').click(this._onRollMentalCrit.bind(this)); + + // Manual Panic button + html.find('.manual-panic-button').click(this._onManualPanic.bind(this)); + html.find('.manual-armor-button').click(this._onManualArmorCheck.bind(this)); + + // Health / Resolve save rolls — click the vital label name + html.find('.vital-save-label').click(this._onVitalSave.bind(this)); + + // Natural weapon attack roll (Morphic Strike, Wraith Teeth & Claw, etc.) + html.find('.natural-weapon-roll-btn').click(this._onNaturalWeaponRoll.bind(this)); + + // Delete injury buttons + html.find('.delete-injury-btn').click(this._onDeleteInjury.bind(this)); + + // Specialty item interactions - click name to open sheet + html.find('.specialty-item .specialty-name').click(ev => { + const itemId = ev.currentTarget.closest('.specialty-item').dataset.itemId; + const item = this.actor.items.get(itemId); + if (item) item.sheet.render(true); + }); + + // Delete specialty from actor + html.find('.delete-specialty-btn').click(async ev => { + ev.stopPropagation(); + const itemId = ev.currentTarget.dataset.itemId; + const item = this.actor.items.get(itemId); + if (item) { + await item.delete(); + ui.notifications.info(`${item.name} removed.`); + } + }); + + // Weapon name click → trigger the roll (same as the ATTACK button) + html.find('.weapon-item .weapon-name').click(this._onWeaponRoll.bind(this)); + + // Roll weapon attack + html.find('.weapon-roll-btn').click(this._onWeaponRoll.bind(this)); + + // REST button — clears weapon ammo lockouts + html.find('.rest-btn').click(this._onRestClick.bind(this)); + + // Delete weapon from actor + html.find('.delete-weapon-btn').click(async ev => { + ev.stopPropagation(); + const itemId = ev.currentTarget.dataset.itemId; + const item = this.actor.items.get(itemId); + if (item) { + await item.delete(); + ui.notifications.info(`${item.name} removed.`); + } + }); + + // Armor interactions - click name to open sheet + html.find('.armor-item .armor-name').click(ev => { + const itemId = ev.currentTarget.closest('.armor-item').dataset.itemId; + const item = this.actor.items.get(itemId); + if (item) item.sheet.render(true); + }); + + // Roll armor check from specific armor row + html.find('.armor-roll-btn').click(async ev => { + ev.preventDefault(); + ev.stopPropagation(); + const itemId = ev.currentTarget.dataset.itemId; + await this._promptAndRollArmorCheck(this.actor, itemId); + }); + + // Delete armor from actor + html.find('.delete-armor-btn').click(async ev => { + ev.stopPropagation(); + const itemId = ev.currentTarget.dataset.itemId; + const item = this.actor.items.get(itemId); + if (item) { + await item.delete(); + ui.notifications.info(`${item.name} removed.`); + } + }); + + // Gear interactions - click name to open sheet + html.find('.gear-item .gear-name').click(ev => { + const itemId = ev.currentTarget.closest('.gear-item').dataset.itemId; + const item = this.actor.items.get(itemId); + if (item) item.sheet.render(true); + }); + + // Drug interactions - click name to open sheet + html.find('.drug-item .drug-name').click(ev => { + const itemId = ev.currentTarget.closest('.drug-item').dataset.itemId; + const item = this.actor.items.get(itemId); + if (item) item.sheet.render(true); + }); + + // Delete gear from actor + html.find('.delete-gear-btn').click(async ev => { + ev.stopPropagation(); + const itemId = ev.currentTarget.dataset.itemId; + const item = this.actor.items.get(itemId); + if (item) { + await item.delete(); + ui.notifications.info(`${item.name} removed.`); + } + }); + + // Activate drug (consumes one dose) + html.find('.drug-activate-btn').click(async ev => { + ev.preventDefault(); + ev.stopPropagation(); + const itemId = ev.currentTarget.dataset.itemId; + const item = this.actor.items.get(itemId); + if (!item) return; + await this._activateDrug(item); + }); + + // Stop active drug (triggers withdrawal if configured) + html.find('.drug-off-btn').click(async ev => { + ev.preventDefault(); + ev.stopPropagation(); + const itemId = ev.currentTarget.dataset.itemId; + const item = this.actor.items.get(itemId); + if (!item) return; + // Two-stage OFF flow: + // 1) Active drug -> enter withdrawal + // 2) Withdrawal active -> clear withdrawal (fully off) + if (item.system?.active) { + await this._deactivateDrug(item); + } else if (item.system?.withdrawalActive) { + await this._clearDrugWithdrawal(item); + } + }); + + // Clear withdrawal state + html.find('.drug-clear-withdrawal-btn').click(async ev => { + ev.preventDefault(); + ev.stopPropagation(); + const itemId = ev.currentTarget.dataset.itemId; + const item = this.actor.items.get(itemId); + if (!item) return; + await this._clearDrugWithdrawal(item); + }); + + // Delete drug + html.find('.delete-drug-btn').click(async ev => { + ev.stopPropagation(); + const itemId = ev.currentTarget.dataset.itemId; + const item = this.actor.items.get(itemId); + if (item) { + await item.delete(); + ui.notifications.info(`${item.name} removed.`); + } + }); + + // Equip/unequip toggle + html.find('.toggle-equip-btn').click(async ev => { + ev.preventDefault(); + ev.stopPropagation(); + const itemId = ev.currentTarget.dataset.itemId; + const item = this.actor.items.get(itemId); + if (!item) return; + + const equipped = item.system?.equipped === true; + await item.update({ "system.equipped": !equipped }); + }); + + // ── EBB FORMULA LISTENERS ───────────────────────────────────── + // Roll an ebb formula (costs flux) + html.find('.ebb-formula-roll-btn').click(this._onRollEbbFormula.bind(this)); + + // Open ebb formula item sheet + html.find('.ebb-formula-edit-btn').click(ev => { + ev.stopPropagation(); + const itemId = ev.currentTarget.dataset.itemId; + const item = this.actor.items.get(itemId); + if (item) item.sheet.render(true); + }); + + // Delete ebb formula from actor + html.find('.ebb-formula-delete-btn').click(async ev => { + ev.stopPropagation(); + const itemId = ev.currentTarget.dataset.itemId; + const item = this.actor.items.get(itemId); + if (item) { + await item.delete(); + ui.notifications.info(`${item.name} removed.`); + } + }); + + // Recover 1 Flux (stretch rest) + html.find('.ebb-flux-recover-btn').click(async ev => { + ev.preventDefault(); + const fluxMax = (parseInt(this.actor.system.skills?.ebb?.value) || 0) > 0 ? (parseInt(this.actor.system.skills.ebb.value) + 1) : (parseInt(this.actor.system.flux?.max) || parseInt(this.actor.system.attributes?.empathy?.value) || 2); + const current = this.actor.system.flux?.value ?? 0; + const next = Math.min(fluxMax, current + 1); + if (next === current) { + ui.notifications.info(`${this.actor.name}'s Flux is already full (${current}/${fluxMax}).`); + return; + } + await this.actor.update({ "system.flux.value": next }); + ui.notifications.info(`${this.actor.name} recovers 1 Flux (${next}/${fluxMax}).`); + }); + + // Full Flux recovery (shift rest) + html.find('.ebb-flux-full-btn').click(async ev => { + ev.preventDefault(); + const fluxMax = (parseInt(this.actor.system.skills?.ebb?.value) || 0) > 0 ? (parseInt(this.actor.system.skills.ebb.value) + 1) : (parseInt(this.actor.system.flux?.max) || parseInt(this.actor.system.attributes?.empathy?.value) || 2); + await this.actor.update({ "system.flux.value": fluxMax }); + ui.notifications.info(`${this.actor.name} fully restores Flux (${fluxMax}/${fluxMax}).`); + }); + + // Clear condition button + html.find('.condition-clear-btn').click(async ev => { + ev.preventDefault(); + ev.stopPropagation(); + const condId = ev.currentTarget.dataset.conditionId; + if (!condId) return; + try { + await this.actor.toggleStatusEffect(condId, { active: false }); + } catch(e) { + // Fallback: find and delete the matching ActiveEffect directly + console.warn(`SLA Zero Two | toggleStatusEffect(false) failed for "${condId}", trying direct delete:`, e); + const effect = this.actor.effects.find(ef => ef.statuses?.has(condId)); + if (effect) await effect.delete(); + } + if (!this.actor.statuses?.has(condId)) { + ui.notifications.info(`Condition cleared: ${condId.replace('sla-','')} on ${this.actor.name}`); + } + }); + + // Vevaphon morph form switching — both the Equipment tab panel AND the Specialties SHIFT buttons + const _onMorphShift = async (ev) => { + ev.preventDefault(); + ev.stopPropagation(); + const form = ev.currentTarget.dataset.form; + // Ensure vevaphon data exists before updating + if (!this.actor.system.vevaphon) { + await this.actor.update({ 'system.vevaphon': { instability: 0, instabilityMax: 12, activeMorphForm: 'none' } }); + } + const instab = Number(this.actor.system.vevaphon?.instability ?? 0); + if (form && form !== 'none') { + const newInstab = Math.min(12, instab + 1); + await this.actor.update({ + 'system.vevaphon.activeMorphForm': form, + 'system.vevaphon.instability': newInstab + }); + const formLabel = form.charAt(0).toUpperCase() + form.slice(1); + ui.notifications.info(`${this.actor.name} shifts to ${formLabel} Form. Instability: ${newInstab}/12`); + // Auto-roll instability check after every shift + await this._rollVevaphonInstabilityCheck(newInstab); + } else { + await this.actor.update({ 'system.vevaphon.activeMorphForm': 'none' }); + ui.notifications.info(`${this.actor.name} returns to base form.`); + } + }; + html.find('.morph-shift-btn').click(_onMorphShift); + + html.find('.morph-form-btn').click(_onMorphShift); + + // Vevaphon instability roll + html.find('.instability-roll-btn').click(async ev => { + ev.preventDefault(); + ev.stopPropagation(); + await this._onRollVevaphonInstability(); + }); + } + + /** + * Build top-of-sheet alerts for active drugs and withdrawal states + * @param {Actor} actor + * @returns {Array<{itemId:string,name:string,duration:string,state:string,summary:string}>} + * @private + */ + _buildDrugAlerts(actor) { + const alerts = []; + for (const item of actor.items || []) { + if (!(item.type === "equipment" && item.system?.isDrug === true)) continue; + const state = item.system?.active ? "active" : (item.system?.withdrawalActive ? "withdrawal" : ""); + if (!state) continue; + const duration = state === "active" + ? String(item.system?.activeDuration || "Duration n/a") + : String(item.system?.withdrawalDuration || "Withdrawal duration n/a"); + const summary = this._formatDrugStateSummary(item, state); + alerts.push({ + itemId: item.id, + name: item.name, + duration: duration, + state: state, + summary: summary || (state === "active" ? "Drug effect active." : "Withdrawal penalties active.") + }); + } + return alerts; + } + + /** + * Read a skill modifier value from drug data object + * @param {object} obj + * @param {string} key + * @returns {number} + * @private + */ + _getDrugSkillModValue(obj, key) { + const value = Number(obj?.[key] ?? 0); + return Number.isFinite(value) ? value : 0; + } + + /** + * Build a compact summary string for active/withdrawal drug state + * @param {Item} drugItem + * @param {"active"|"withdrawal"} state + * @returns {string} + * @private + */ + _formatDrugStateSummary(drugItem, state = "active") { + const s = drugItem?.system || {}; + const isWithdrawal = state === "withdrawal"; + const health = Number(isWithdrawal ? s.wdHealthMod : s.healthMod) || 0; + const resolve = Number(isWithdrawal ? s.wdResolveMod : s.resolveMod) || 0; + const stat = Number(isWithdrawal ? s.wdStatPhysicalMod : s.statPhysicalMod) || 0; + const skillAll = Number(isWithdrawal ? s.wdSkillAllMod : s.skillAllMod) || 0; + const panicReduction = Number(isWithdrawal ? s.wdPanicReduction : s.panicReduction) || 0; + const injuryIgnore = Number(isWithdrawal ? s.wdInjuryPenaltyIgnore : s.injuryPenaltyIgnore) || 0; + + const healthHeal = (!isWithdrawal) ? (Number(s.healthHeal) || 0) : 0; + + const parts = []; + if (healthHeal > 0) parts.push(`Heals ${healthHeal} HP`); + if (health !== 0) parts.push(`Max Health ${health > 0 ? "+" : ""}${health}`); + if (resolve !== 0) parts.push(`Resolve ${resolve > 0 ? "+" : ""}${resolve}`); + if (stat !== 0) parts.push(`Physical ${stat > 0 ? "+" : ""}${stat}`); + if (skillAll !== 0) parts.push(`All Skills ${skillAll > 0 ? "+" : ""}${skillAll}`); + if (panicReduction > 0) parts.push(`Panic -${panicReduction}`); + if (injuryIgnore > 0) parts.push(`Ignore injury penalties up to ${injuryIgnore}`); + + const skillObj = isWithdrawal ? s.wdSkillMods : s.skillMods; + const namedSkills = []; + for (const key of ["force", "melee", "stamina", "marksmanship", "mobility", "stealth", "crafting", "observation", "survival", "healing", "insight", "persuasion"]) { + const value = this._getDrugSkillModValue(skillObj, key); + if (value !== 0) namedSkills.push(`${key} ${value > 0 ? "+" : ""}${value}`); + } + if (namedSkills.length > 0) parts.push(namedSkills.join(", ")); + + return parts.join(" | "); + } + + /** + * Notify GM and table when drug state changes + * @param {Item} item + * @param {string} stateLabel + * @param {string} duration + * @private + */ + async _notifyDrugState(item, stateLabel, duration = "") { + const actorName = this.actor?.name || "Unknown Actor"; + const itemName = item?.name || "Drug"; + const message = `${actorName}: ${itemName} ${stateLabel}${duration ? ` (${duration})` : ""}`; + + ui.notifications.info(message); + if (game.user?.isGM) { + ui.notifications.warn(`Drug alert - ${message}`); + } + + const gmIds = game.users + .filter(u => u.isGM) + .map(u => u.id); + if (gmIds.length > 0) { + await ChatMessage.create({ + speaker: ChatMessage.getSpeaker({ actor: this.actor }), + whisper: gmIds, + content: `
Drug Alert: ${message}
` + }); + } + } + + /** + * Activate a drug item and consume one dose + * @param {Item} item + * @returns {Promise} + * @private + */ + /** + * Compute the current health/resolve "contribution" for this drug given a state. + * Used to derive the delta when state transitions, so we can keep CURRENT HP in + * sync with the max-HP swing (per Tim, drug +2 max also gives +2 current; when + * the drug ends, both drop by 2). + * @private + */ + _drugStatContribution(item, state) { + if (state === "active") return { + h: Number(item.system?.healthMod ?? 0) || 0, + r: Number(item.system?.resolveMod ?? 0) || 0 + }; + if (state === "withdrawal") return { + h: Number(item.system?.wdHealthMod ?? 0) || 0, + r: Number(item.system?.wdResolveMod ?? 0) || 0 + }; + return { h: 0, r: 0 }; + } + + /** + * Apply a delta to current HP and Resolve, clamped to [0, max]. Called after a + * drug state change so the player's "live" pool tracks the max swing. + * @private + */ + async _applyCurrentStatDelta(hDelta, rDelta) { + if (!this.actor || (!hDelta && !rDelta)) return; + const updates = {}; + + if (hDelta) { + const rawHp = this.actor.system?.derivedStats?.health; + const curHp = Number((typeof rawHp === "object" ? rawHp?.value : rawHp) ?? 0); + const hpMax = Number(this.actor.system?.derivedStats?.health?.max ?? 0); + const newHp = Math.max(0, Math.min(hpMax, curHp + hDelta)); + if (newHp !== curHp) updates["system.derivedStats.health.value"] = newHp; + } + + if (rDelta) { + const rawRes = this.actor.system?.derivedStats?.resolve; + const curRes = Number((typeof rawRes === "object" ? rawRes?.value : rawRes) ?? 0); + const resMax = Number(this.actor.system?.derivedStats?.resolve?.max ?? 0); + const newRes = Math.max(0, Math.min(resMax, curRes + rDelta)); + if (newRes !== curRes) updates["system.derivedStats.resolve.value"] = newRes; + } + + if (Object.keys(updates).length > 0) await this.actor.update(updates); + } + + async _activateDrug(item) { + if (!item || !(item.type === "equipment" && item.system?.isDrug === true)) return; + const qty = Math.max(0, Number(item.system?.quantity) || 0); + if (qty <= 0) { + ui.notifications.warn(`${item.name} has no remaining doses.`); + return; + } + + // Capture state BEFORE update so we can compute the contribution delta + const oldState = item.system?.active ? "active" + : item.system?.withdrawalActive ? "withdrawal" : "off"; + const oldContrib = this._drugStatContribution(item, oldState); + + await item.update({ + "system.quantity": qty - 1, + "system.active": true, + "system.withdrawalActive": false + }); + + // ── Ensure max is recalculated before we mirror the swing to current ──── + if (typeof recalcDerivedStats === "function") { + try { await recalcDerivedStats(this.actor); } catch(_) {} + } + + // ── Mirror max swing to current HP/Resolve ───────────────────────────── + // e.g. drug.healthMod = +2 with old state "off" → +2 current HP (capped at new max). + const newContrib = this._drugStatContribution(item, "active"); + await this._applyCurrentStatDelta(newContrib.h - oldContrib.h, newContrib.r - oldContrib.r); + + // ── Deduct drug cost from credits ──────────────────────────────────────── + const drugCost = Math.max(0, Number(item.system?.cost) || 0); + if (drugCost > 0) { + const currentCredits = Number(this.actor.system?.details?.credits ?? 0); + const newCredits = Math.max(0, currentCredits - drugCost); + await this.actor.update({ "system.details.credits": newCredits }); + ui.notifications.info( + `${item.name}: ${drugCost}¢ charged to ${this.actor.name}. Credits: ${currentCredits} → ${newCredits}` + ); + } + + const stressBonus = Math.max(0, Number(item.system?.stressRecoveryBonus) || 0); + if (stressBonus > 0) { + const currentStress = this._getStressValue(this.actor); + const nextStress = Math.max(0, currentStress - stressBonus); + if (nextStress !== currentStress) { + await this.actor.update(this._buildStressUpdate(this.actor, nextStress)); + } + } + + // ── Instant HP heal (healthHeal): restore current HP, clamped at max ──── + // NOTE: healthHeal NEVER raises max — only current. This is the "medkit" + // path. Drugs that raise max should set healthMod, not healthHeal. + const healthHeal = Math.max(0, Number(item.system?.healthHeal) || 0); + if (healthHeal > 0 && this.actor) { + const rawHp = this.actor.system?.derivedStats?.health; + const currentHp = Number((typeof rawHp === "object" ? rawHp?.value : rawHp) ?? 0); + const hpMax = Number(this.actor.system?.derivedStats?.health?.max ?? 0); + if (hpMax > 0) { + const newHp = Math.min(hpMax, currentHp + healthHeal); + const actual = newHp - currentHp; + if (actual > 0) { + await this.actor.update({ "system.derivedStats.health.value": newHp }); + ui.notifications.info(`${item.name}: healed ${actual} HP (${currentHp} → ${newHp} / ${hpMax})`); + } + } + } + + await this._notifyDrugState(item, "is ACTIVE", String(item.system?.activeDuration || "")); + } + + /** + * Deactivate an active drug and start withdrawal state when defined + * @param {Item} item + * @returns {Promise} + * @private + */ + async _deactivateDrug(item) { + if (!item || !(item.type === "equipment" && item.system?.isDrug === true)) return; + + const s = item.system || {}; + const hasWithdrawal = Boolean(s.withdrawalDuration) || + Number(s.wdHealthMod || 0) !== 0 || + Number(s.wdResolveMod || 0) !== 0 || + Number(s.wdStatPhysicalMod || 0) !== 0 || + Number(s.wdSkillAllMod || 0) !== 0 || + Number(s.wdPanicReduction || 0) !== 0 || + Object.values(s.wdSkillMods || {}).some(v => Number(v || 0) !== 0); + + // Capture state BEFORE update so we can compute the contribution delta + const oldState = item.system?.active ? "active" + : item.system?.withdrawalActive ? "withdrawal" : "off"; + const oldContrib = this._drugStatContribution(item, oldState); + + await item.update({ + "system.active": false, + "system.withdrawalActive": hasWithdrawal + }); + + // ── Ensure max is recalculated before we mirror the swing to current ──── + if (typeof recalcDerivedStats === "function") { + try { await recalcDerivedStats(this.actor); } catch(_) {} + } + + // ── Mirror max swing to current HP/Resolve ───────────────────────────── + // Drug ending (e.g. +2 active → 0) drops current HP by 2 (floor at 0). + const newState = hasWithdrawal ? "withdrawal" : "off"; + const newContrib = this._drugStatContribution(item, newState); + await this._applyCurrentStatDelta(newContrib.h - oldContrib.h, newContrib.r - oldContrib.r); + + if (hasWithdrawal) { + await this._notifyDrugState(item, "entered WITHDRAWAL", String(item.system?.withdrawalDuration || "")); + const strainDamage = Math.max(0, Number(item.system?.wdPostUseStaminaDamage) || 0); + if (strainDamage > 0) { + await this._runDrugStaminaStrainCheck(item, strainDamage); + } + } else { + await this._notifyDrugState(item, "is OFF", ""); + } + } + + /** + * Clear a drug withdrawal state manually + * @param {Item} item + * @returns {Promise} + * @private + */ + async _clearDrugWithdrawal(item) { + if (!item || !(item.type === "equipment" && item.system?.isDrug === true)) return; + + // Capture state BEFORE update + const oldState = item.system?.active ? "active" + : item.system?.withdrawalActive ? "withdrawal" : "off"; + const oldContrib = this._drugStatContribution(item, oldState); + + await item.update({ "system.withdrawalActive": false }); + + if (typeof recalcDerivedStats === "function") { + try { await recalcDerivedStats(this.actor); } catch(_) {} + } + + const newContrib = this._drugStatContribution(item, "off"); + await this._applyCurrentStatDelta(newContrib.h - oldContrib.h, newContrib.r - oldContrib.r); + + await this._notifyDrugState(item, "withdrawal cleared", ""); + } + + /** + * Run post-drug stamina check and apply strain damage on failure + * @param {Item} item + * @param {number} failDamage + * @returns {Promise} + * @private + */ + async _runDrugStaminaStrainCheck(item, failDamage) { + const str = Number(this.actor.system?.attributes?.strength?.value || 0); + const stamina = Number(this.actor.system?.skills?.stamina?.value || 0); + const pool = Math.max(0, str + stamina); + if (pool <= 0) return; + + const roll = await (new Roll(`${pool}d6`)).evaluate(); + if (game.dice3d) { + await game.dice3d.showForRoll(roll, game.user, true); + } + const successes = (roll.dice?.[0]?.results || []).filter(r => r.result === 6).length; + const failed = successes <= 0; + + let healthLine = ""; + if (failed) { + const currentHealth = this._getDerivedStatValue(this.actor, "health"); + const nextHealth = Math.max(0, currentHealth - failDamage); + const updates = this._buildDerivedStatUpdate(this.actor, "health", nextHealth); + if (nextHealth <= 0) updates["system.derivedStats.broken"] = true; + await this.actor.update(updates); + healthLine = `
Failure: ${this.actor.name} takes ${failDamage} damage ignoring armor (${currentHealth} -> ${nextHealth} Health).
`; + } else { + healthLine = `
Success: No strain damage.
`; + } + + await ChatMessage.create({ + speaker: ChatMessage.getSpeaker({ actor: this.actor }), + content: `
${item.name} - Stamina Strain Check
Pool: ${pool}d6 | Successes: ${successes}
${healthLine}
` + }); + } + + /** + * Handle attribute increment/decrement + * @param {Event} event + * @private + */ + async _onAttributeChange(event) { + event.preventDefault(); + event.stopPropagation(); + + const button = event.currentTarget; + const action = button.dataset.action; + const attribute = button.dataset.attribute; + const currentValue = parseInt(this.actor.system.attributes[attribute].value) || 2; + + let newValue = currentValue; + if (action === 'increment') { + newValue = Math.min(5, currentValue + 1); + } else if (action === 'decrement') { + newValue = Math.max(2, currentValue - 1); + } + + if (newValue !== currentValue) { + await this.actor.update({ + [`system.attributes.${attribute}.value`]: newValue + }); + } + } + + /** + * Handle skill increment/decrement + * @param {Event} event + * @private + */ + async _onSkillChange(event) { + event.preventDefault(); + event.stopPropagation(); + + const button = event.currentTarget; + const isIncrement = button.classList.contains('skill-increment'); + const skill = button.dataset.skill; + const currentValue = parseInt(this.actor.system.skills[skill].value) || 0; + + let newValue = currentValue; + if (isIncrement) { + newValue = Math.min(5, currentValue + 1); + } else { + newValue = Math.max(0, currentValue - 1); + } + + if (newValue !== currentValue) { + await this.actor.update({ + [`system.skills.${skill}.value`]: newValue + }); + } + } + + /** + * Handle skill roll button clicks + * @param {Event} event + * @private + */ + async _onSkillRoll(event) { + event.preventDefault(); + event.stopPropagation(); + + const button = event.currentTarget; + const skill = button.dataset.skill; + const attribute = button.dataset.attribute; + + const skillValue = parseInt(this.actor.system.skills?.[skill]?.value) || 0; + const rawAttrRoll = this.actor.system.attributes?.[attribute]; + const attributeValue = (typeof rawAttrRoll === 'object' ? parseInt(rawAttrRoll?.value) : parseInt(rawAttrRoll)) || 2; + const armorMods = this._getArmorRollModifiers(this.actor, { attribute, skill }); + const drugMods = this._getDrugRollModifiers(this.actor, { attribute, skill }); + const racialMods = this._getRacialDiceBonus(this.actor, { skill, attribute }); + const condMods = _getConditionModifiers(this.actor, { skill, attribute }); + const totalDice = Math.max(0, attributeValue + skillValue + armorMods.total + drugMods.total + racialMods.bonus + condMods.bonus); + + const skillLabel = skill.charAt(0).toUpperCase() + skill.slice(1); + const label = `${skillLabel} (${attribute})`; + const formula = `${skillLabel} (${attribute} ${attributeValue} + Skill ${skillValue}` + + `${armorMods.statMod !== 0 ? ` + Armor Stat ${armorMods.statMod}` : ''}` + + `${armorMods.skillMod !== 0 ? ` + Armor Skill ${armorMods.skillMod}` : ''}` + + `${drugMods.statMod !== 0 ? ` + Drug Stat ${drugMods.statMod}` : ''}` + + `${drugMods.skillMod !== 0 ? ` + Drug Skill ${drugMods.skillMod}` : ''}` + + `${racialMods.bonus !== 0 ? ` + Racial/Drug ${racialMods.bonus > 0 ? '+' : ''}${racialMods.bonus}` : ''}` + + `${condMods.bonus !== 0 ? ` + Conditions ${condMods.bonus}` : ''}` + + ` = ${totalDice}d6)`; + + return this._rollYZEDice(totalDice, label, formula, skill); + } + + /** + * Handle attribute roll button clicks + * @param {Event} event + * @private + */ + async _onAttributeRoll(event) { + event.preventDefault(); + event.stopPropagation(); + + const button = event.currentTarget; + const attribute = button.dataset.attribute; + + const attributeValue = parseInt(this.actor.system.attributes[attribute]?.value) || 2; + const armorMods = this._getArmorRollModifiers(this.actor, { attribute }); + const drugMods = this._getDrugRollModifiers(this.actor, { attribute }); + const racialMods = this._getRacialDiceBonus(this.actor, { attribute }); + const condMods = _getConditionModifiers(this.actor, { attribute }); + const totalDice = Math.max(0, attributeValue + armorMods.total + drugMods.total + racialMods.bonus + condMods.bonus); + const label = attribute.charAt(0).toUpperCase() + attribute.slice(1); + const formula = `${label} ${attributeValue}` + + `${armorMods.statMod !== 0 ? ` + Armor Stat ${armorMods.statMod}` : ''}` + + `${drugMods.statMod !== 0 ? ` + Drug Stat ${drugMods.statMod}` : ''}` + + `${racialMods.bonus !== 0 ? ` + Racial/Drug ${racialMods.bonus > 0 ? '+' : ''}${racialMods.bonus}` : ''}` + + `${condMods.bonus !== 0 ? ` + Conditions ${condMods.bonus}` : ''}` + + ` = ${totalDice}d6`; + + return this._rollYZEDice(totalDice, label, formula, attribute); + } + + /** + * Handle Physical Critical Injury button click + * @param {Event} event + * @private + */ + async _onRollPhysicalCrit(event) { + event.preventDefault(); + event.stopPropagation(); + + // Guard: if auto-crit already fired recently, confirm before rolling again + if (_critCooldownActive(this.actor.id, "physical")) { + const roll = await Dialog.confirm({ + title: "Critical Already Rolled", + content: "

A Physical Critical was already rolled for this character recently (auto-triggered when HP hit 0). Roll another one?

", + yes: () => true, no: () => false, defaultYes: false + }); + if (!roll) return; + } + + _critRecordFired(this.actor.id, "physical"); + await this.actor.update({ 'system.derivedStats.broken': true }); + await this._rollPhysicalCritical(this.actor); + ui.notifications.warn(`${this.actor.name} is BROKEN and suffers a Physical Critical Injury!`); + } + + /** + * Handle Mental Critical Injury button click + * @param {Event} event + * @private + */ + async _onRollMentalCrit(event) { + event.preventDefault(); + event.stopPropagation(); + + // Guard: if auto-crit already fired recently, confirm before rolling again + if (_critCooldownActive(this.actor.id, "mental")) { + const roll = await Dialog.confirm({ + title: "Critical Already Rolled", + content: "

A Mental Critical was already rolled for this character recently (auto-triggered when Resolve hit 0). Roll another one?

", + yes: () => true, no: () => false, defaultYes: false + }); + if (!roll) return; + } + + _critRecordFired(this.actor.id, "mental"); + await this.actor.update({ 'system.derivedStats.broken': true }); + await this._rollMentalCritical(this.actor); + ui.notifications.warn(`${this.actor.name} is BROKEN and suffers a Mental Critical Injury!`); + } + + /** + * Roll an Ebb formula — spend flux, roll Empathy + Ebb skill, post result + * YZE EBB rules: + * - Roll Empathy dice + Ebb skill dice (6s = successes) + * - Need formula.successes successes to fully activate + * - Flux is spent on activation regardless of success + * - Can push the roll (gains stress; Brain Wasters risk catastrophe) + * @param {Event} event + * @private + */ + async _onRollEbbFormula(event) { + event.preventDefault(); + event.stopPropagation(); + + const button = event.currentTarget; + const itemId = button.dataset.itemId; + const item = this.actor.items.get(itemId); + if (!item) return; + + const formula = item.system; + const fluxCost = Number(formula.fluxCost ?? 1); + const currentFlux = this.actor.system.flux?.value ?? 0; + const fluxMax = (parseInt(this.actor.system.skills?.ebb?.value) || 0) > 0 ? (parseInt(this.actor.system.skills.ebb.value) + 1) : (parseInt(this.actor.system.flux?.max) || parseInt(this.actor.system.attributes?.empathy?.value) || 2); + + // Check sufficient flux + if (currentFlux < fluxCost) { + ui.notifications.warn( + `Not enough Flux to use ${item.name}! ` + + `Needs ${fluxCost}, have ${currentFlux}/${fluxMax}.` + ); + return; + } + + // Deduct flux before rolling + const newFlux = Math.max(0, currentFlux - fluxCost); + await this.actor.update({ "system.flux.value": newFlux }); + + // Build dice pool: Empathy attribute + Ebb skill + // Use parseInt() || 0 — robust against corrupt stored values ("3,3", "NaN", etc.) + const empathy = parseInt(this.actor.system.attributes?.empathy?.value) || 2; + const ebbSkill = parseInt(this.actor.system.skills?.ebb?.value) || 0; + const condMods = _getConditionModifiers(this.actor, { attribute: 'empathy', skill: 'ebb' }); + const totalDice = Math.max(0, empathy + ebbSkill + condMods.bonus) || 1; // fallback to 1 die minimum + const requiredSuccesses = Number(formula.successes ?? 1); + const disciplineLabel = formula.discipline + ? formula.discipline.charAt(0).toUpperCase() + formula.discipline.slice(1) + : "Ebb"; + + const label = `${item.name} [${disciplineLabel}]`; + const formulaStr = `Empathy ${empathy} + Ebb ${ebbSkill}` + + `${condMods.bonus !== 0 ? ` + Conditions ${condMods.bonus}` : ''}` + + ` = ${totalDice}d6 | ` + + `Flux: ${currentFlux} → ${newFlux} (cost ${fluxCost}) | Need ${requiredSuccesses} suc`; + + const ebbInfo = { + itemId: item.id, + itemName: item.name, + effect: formula.effect || "", + catastrophe: formula.catastrophe || "", + requiredSuccesses, + discipline: formula.discipline || "ebb", + range: formula.range || "", + duration: formula.duration || "" + }; + + return this._rollYZEDice(totalDice, label, formulaStr, "empathy", null, { ebbInfo }); + } + + /** + * Auto-apply deterministic EBB effects to the actor + * @param {object} ebbInfo - The ebb formula metadata + * @param {number} successes - Number of successes rolled + * @private + */ + async _applyEbbEffect(ebbInfo, successes) { + if (!ebbInfo || successes < ebbInfo.requiredSuccesses) return; + const actor = this.actor; + const itemName = (ebbInfo.itemName || "").toLowerCase(); + const discipline = (ebbInfo.discipline || "").toLowerCase(); + + if (discipline === "heal") { + // v1.7.6: fix phantom write path — use derivedStats.health (the real schema). + // Healing restores CURRENT HP only, never raises max. + const rawHp = actor.system?.derivedStats?.health; + const hpCurrent = Number((typeof rawHp === "object" ? rawHp?.value : rawHp) ?? 0); + const hpMax = Number(actor.system?.derivedStats?.health?.max ?? 0) || hpCurrent; + const stress = this._getStressValue(actor); + + if (itemName.includes("mend")) { + // Mend: restore min(2, successes) HP + const healAmt = Math.min(2, successes); + const newHp = Math.min(hpMax, hpCurrent + healAmt); + if (newHp > hpCurrent) { + await actor.update({ "system.derivedStats.health.value": newHp }); + ui.notifications.info(`${actor.name} | Mend: +${newHp - hpCurrent} HP restored (${newHp}/${hpMax}).`); + } + } else if (itemName.includes("restore")) { + // Restore: heal successes + 1 HP + const healAmt = successes + 1; + const newHp = Math.min(hpMax, hpCurrent + healAmt); + if (newHp > hpCurrent) { + await actor.update({ "system.derivedStats.health.value": newHp }); + ui.notifications.info(`${actor.name} | Restore: +${newHp - hpCurrent} HP restored (${newHp}/${hpMax}).`); + } + } else if (itemName.includes("calm")) { + // Calm: reduce stress by 1 + const newStress = Math.max(0, stress - 1); + if (newStress < stress) { + await actor.update(this._buildStressUpdate(actor, newStress)); + ui.notifications.info(`${actor.name} | Calm: stress reduced to ${newStress}.`); + } + } + } + } + + /** + * Handle a natural weapon attack roll from a specialty (Morphic Strike, Wraith Teeth & Claw, etc.) + * Uses STR + Melee, same pipeline as a melee weapon attack, with the weapon's own DMG/AP/ROF. + * @param {Event} event + * @private + */ + async _onNaturalWeaponRoll(event) { + event.preventDefault(); + event.stopPropagation(); + + const itemId = event.currentTarget.dataset.itemId; + const item = this.actor.items.get(itemId); + if (!item) return; + + const s = item.system; + const damage = Number(s.naturalWeaponDamage) || 2; + const ap = Number(s.naturalWeaponAP) || 1; + const rof = Number(s.naturalWeaponROF) || 2; + + const attrValue = parseInt(this.actor.system.attributes?.strength?.value) || 2; + const skillValue = parseInt(this.actor.system.skills?.melee?.value) || 0; + const armorMods = this._getArmorRollModifiers(this.actor, { attribute: 'strength', skill: 'melee' }); + const drugMods = this._getDrugRollModifiers(this.actor, { attribute: 'strength', skill: 'melee' }); + const racialMods = this._getRacialDiceBonus(this.actor, { skill: 'melee', attribute: 'strength' }); + const condMods = _getConditionModifiers(this.actor, { attribute: 'strength', skill: 'melee' }); + const baseDice = attrValue + skillValue + armorMods.total + drugMods.total + racialMods.bonus + condMods.bonus; + + const formulaStr = `STR ${attrValue} + Melee ${skillValue}` + + `${armorMods.total !== 0 ? ` + Armor ${armorMods.total}` : ''}` + + `${drugMods.total !== 0 ? ` + Drug ${drugMods.total}` : ''}` + + `${racialMods.bonus !== 0 ? ` + Racial ${racialMods.bonus}` : ''}` + + `${condMods.bonus !== 0 ? ` + Conditions ${condMods.bonus}` : ''}` + + ` = ${baseDice}d6 | DMG ${damage}, AP ${ap}, ROF ${rof}`; + + const weaponInfo = { + name: item.name, + damage, + ap, + range: 'Engaged', + rof, + category: 'melee', + weaponType: 'Natural' + }; + + return this._rollYZEDice(baseDice, item.name, formulaStr, 'strength', weaponInfo); + } + + /** + * Handle Health or Resolve save roll — clicking the vital label name. + * Rolls current value in plain d6 (no stress dice, no dialog). + * A failure (zero successes) adds +1 Stress. + * @param {Event} event + * @private + */ + async _onVitalSave(event) { + event.preventDefault(); + event.stopPropagation(); + + const saveType = event.currentTarget.dataset.saveType; // "health" or "resolve" + const label = saveType === "health" ? "Health Save" : "Resolve Save"; + const current = Number(this.actor.system.derivedStats?.[saveType]?.value) || 0; + + if (current <= 0) { + ui.notifications.warn(`${this.actor.name} has no ${saveType} remaining — cannot make a ${label}.`); + return; + } + + // Apply condition modifiers — conditions penalise all rolls including saves + const condMods = _getConditionModifiers(this.actor); + const pool = Math.max(0, current + condMods.bonus); + const condNote = condMods.bonus !== 0 + ? ` + Conditions ${condMods.bonus} = ${pool}d6` + : ''; + + if (pool <= 0) { + ui.notifications.warn(`${this.actor.name}: condition penalties reduce the ${label} pool to 0 — automatic failure. +1 Stress.`); + const curStress = this._getStressValue(this.actor); + const newStress = Math.min(10, curStress + 1); + await this.actor.update(this._buildStressUpdate(this.actor, newStress)); + await ChatMessage.create({ + speaker: ChatMessage.getSpeaker({ actor: this.actor }), + flavor: `${this.actor.name} — ${label}`, + content: `
AUTO-FAIL — 0 dice after condition penalties. +1 Stress (now ${newStress}).
`, + }); + return; + } + + // Roll the conditioned pool in d6 — no stress dice, no dialog + const roll = new Roll(`${pool}d6`); + await roll.evaluate(); + + if (game.dice3d) await game.dice3d.showForRoll(roll, game.user, true); + + const results = roll.dice[0]?.results || []; + const successes = results.filter(r => r.result === 6).length; + const failed = successes === 0; + + // Build dice display using existing yze-die classes + const diceHtml = results.map(r => { + let cls = 'yze-die'; + if (r.result === 6) cls += ' yze-success'; + return `
${r.result}
`; + }).join(''); + + let resultLine; + if (failed) { + const curStress = this._getStressValue(this.actor); + const newStress = Math.min(10, curStress + 1); + await this.actor.update(this._buildStressUpdate(this.actor, newStress)); + resultLine = `
FAILED — no successes. +1 Stress (now ${newStress}).
`; + } else { + resultLine = `
SUCCESS — ${successes} success${successes !== 1 ? 'es' : ''}!
`; + } + + await ChatMessage.create({ + speaker: ChatMessage.getSpeaker({ actor: this.actor }), + flavor: `${this.actor.name} — ${label}
${current}d6${condNote}`, + content: `
${diceHtml}
${resultLine}
`, + style: CONST.CHAT_MESSAGE_STYLES?.ROLL, + type: CONST.CHAT_MESSAGE_TYPES?.ROLL, + roll: roll, + rollMode: game.settings.get('core', 'rollMode') + }); + } + + /** + * Handle Manual Panic button click + * @param {Event} event + * @private + */ + async _onManualPanic(event) { + event.preventDefault(); + event.stopPropagation(); + + // Roll Panic check + await this._rollPanic(this.actor); + } + + /** + * Handle Manual Armor Check button click + * @param {Event} event + * @private + */ + async _onManualArmorCheck(event) { + event.preventDefault(); + event.stopPropagation(); + await this._promptAndRollArmorCheck(this.actor); + } + + /** + * Handle delete injury button click + * @param {Event} event + * @private + */ + async _onDeleteInjury(event) { + event.preventDefault(); + event.stopPropagation(); + + const button = event.currentTarget; + const injuryType = button.dataset.injuryType; // 'physical' or 'mental' + const injuryIndex = parseInt(button.dataset.injuryIndex); + + // Get current injuries array + const injuries = [...this.actor.system.criticalInjuries[injuryType]]; + + // Remove the injury at the specified index + injuries.splice(injuryIndex, 1); + + // Update actor + await this.actor.update({ + [`system.criticalInjuries.${injuryType}`]: injuries + }); + + ui.notifications.info(`Critical injury removed from ${this.actor.name}`); + } + + /** + * Handle double-click to edit attributes + * @param {Event} event + * @private + */ + _onEditAttribute(event) { + event.stopPropagation(); + const box = $(event.currentTarget); + const input = box.find('input'); + const valueDiv = box.find('.attr-value'); + + valueDiv.hide(); + input.show().focus().select(); + + input.one('blur', () => { + input.hide(); + valueDiv.show(); + }); + } + + /** + * Handle double-click to edit skills + * @param {Event} event + * @private + */ + _onEditSkill(event) { + event.stopPropagation(); + const row = $(event.currentTarget); + const input = row.find('input'); + const valueDiv = row.find('.skill-value'); + + valueDiv.hide(); + input.show().focus().select(); + + input.one('blur', () => { + input.hide(); + valueDiv.show(); + }); + } + + /** + * Handle clickable rolls + * @param {Event} event The originating click event + * @private + */ + async _onRoll(event) { + event.preventDefault(); + const element = event.currentTarget; + const dataset = element.dataset; + + // Handle attribute rolls + if (dataset.rollType === 'attribute') { + const attribute = dataset.attribute; + + // Check if attribute exists and has a value + if (!this.actor.system.attributes || !this.actor.system.attributes[attribute]) { + ui.notifications.error(`Attribute ${attribute} not found. Please recreate this character with the updated system.`); + return; + } + + const attributeValue = this.actor.system.attributes[attribute].value || 2; + const armorMods = this._getArmorRollModifiers(this.actor, { attribute }); + const drugMods = this._getDrugRollModifiers(this.actor, { attribute }); + const condMods = _getConditionModifiers(this.actor, { attribute }); + const totalDice = Math.max(0, attributeValue + armorMods.total + drugMods.total + condMods.bonus); + const label = attribute.charAt(0).toUpperCase() + attribute.slice(1); + const formula = `${label} ${attributeValue}` + + `${armorMods.statMod !== 0 ? ` + Armor Stat ${armorMods.statMod}` : ''}` + + `${drugMods.statMod !== 0 ? ` + Drug Stat ${drugMods.statMod}` : ''}` + + `${condMods.bonus !== 0 ? ` + Conditions ${condMods.bonus}` : ''}` + + ` = ${totalDice}d6`; + + return this._rollYZEDice(totalDice, label, formula, attribute); + } + + // Handle skill rolls + if (dataset.rollType === 'skill') { + const skill = dataset.skill; + const attribute = dataset.attribute; + + // Check if skill and attribute exist + if (!this.actor.system.skills || !this.actor.system.skills[skill]) { + ui.notifications.error(`Skill ${skill} not found. Please recreate this character with the updated system.`); + return; + } + if (!this.actor.system.attributes || !this.actor.system.attributes[attribute]) { + ui.notifications.error(`Attribute ${attribute} not found. Please recreate this character with the updated system.`); + return; + } + + const skillValue = this.actor.system.skills[skill].value || 0; + const attributeValue = this.actor.system.attributes[attribute].value || 2; + const armorMods = this._getArmorRollModifiers(this.actor, { attribute, skill }); + const drugMods = this._getDrugRollModifiers(this.actor, { attribute, skill }); + const condMods = _getConditionModifiers(this.actor, { attribute, skill }); + const totalDice = Math.max(0, attributeValue + skillValue + armorMods.total + drugMods.total + condMods.bonus); + const attrLabel = attribute.charAt(0).toUpperCase() + attribute.slice(1); + const skillLabel = skill.charAt(0).toUpperCase() + skill.slice(1); + const label = `${skillLabel} (${attribute.substring(0, 3).toUpperCase()})`; + const formula = `${attrLabel} ${attributeValue} + ${skillLabel} ${skillValue}` + + `${armorMods.statMod !== 0 ? ` + Armor Stat ${armorMods.statMod}` : ''}` + + `${armorMods.skillMod !== 0 ? ` + Armor Skill ${armorMods.skillMod}` : ''}` + + `${drugMods.statMod !== 0 ? ` + Drug Stat ${drugMods.statMod}` : ''}` + + `${drugMods.skillMod !== 0 ? ` + Drug Skill ${drugMods.skillMod}` : ''}` + + `${condMods.bonus !== 0 ? ` + Conditions ${condMods.bonus}` : ''}` + + ` = ${totalDice}d6`; + + return this._rollYZEDice(totalDice, label, formula, attribute); + } + } + + /** + * Handle weapon attack roll + * Ranged weapons use AGI + Marksmanship + Gear Bonus + * Melee weapons use STR + Melee + Gear Bonus + * @param {Event} event + * @private + */ + async _onWeaponRoll(event) { + event.preventDefault(); + event.stopPropagation(); + + const button = event.currentTarget; + // Roll button has data-item-id directly; weapon name span does not — walk up to .weapon-item + const itemId = button.dataset.itemId ?? button.closest('.weapon-item')?.dataset.itemId; + const item = this.actor.items.get(itemId); + if (!item) return; + + const weaponData = item.system; + const isMelee = (weaponData.category === "melee") || + String(weaponData.range ?? "").toLowerCase() === "engaged"; + + // If ranged weapon is out of ammo (locked by a failed push), block firing + if (!isMelee && weaponData.ammoEmpty) { + ui.notifications.warn(`${item.name} is out of ammo — rest to restore ammunition.`); + return; + } + + // Determine attribute + skill + const attribute = isMelee ? "strength" : "agility"; + const skill = isMelee ? "melee" : "marksmanship"; + + // Attributes: characters store { value: N }, NPCs store a flat number. + const rawAttr = this.actor.system.attributes?.[attribute]; + const attrValue = (typeof rawAttr === 'object' ? parseInt(rawAttr?.value) : parseInt(rawAttr)) || 2; + // Skills: NPC actors have no skills object — fall back to 0. + const skillValue = parseInt(this.actor.system.skills?.[skill]?.value) || 0; + const attrLabel = attribute.charAt(0).toUpperCase() + attribute.slice(1); + const skillLabel = skill.charAt(0).toUpperCase() + skill.slice(1); + const gearBonus = parseInt(weaponData.gearBonus) || 0; + const armorMods = this._getArmorRollModifiers(this.actor, { attribute, skill }); + const drugMods = this._getDrugRollModifiers(this.actor, { attribute, skill }); + // Pass weaponType for Frother blade proficiency and similar weapon-gated racial bonuses + const racialMods = this._getRacialDiceBonus(this.actor, { skill, attribute, weaponType: weaponData.weaponType || "" }); + const condMods = _getConditionModifiers(this.actor, { skill, attribute }); + let baseDice = attrValue + skillValue + gearBonus + armorMods.total + drugMods.total + racialMods.bonus + condMods.bonus; + + // Multi-shot vars — set from dialog result below (inside !isMelee block) + let shotBonus = 0; + let shotsChosen = 1; + + // Fire mode / ammo type (ranged only) + let fireMode = "single"; + let ammoType = "standard"; + let modeBonusDice = 0; + let ammoBonusDice = 0; + let modeDamageBonus = 0; + let ammoDamageBonus = 0; + let ammoSpent = 0; + let rangeBonus = 0; + let scopeBonus = 0; + let aiBonus = 0; + + if (!isMelee) { + const rawFireModes = Array.isArray(weaponData.fireModes) + ? weaponData.fireModes + : (typeof weaponData.fireModes === "string" + ? weaponData.fireModes.split(",").map(s => s.trim()).filter(Boolean) + : (weaponData.fireModes ? [weaponData.fireModes] : ["single"])); + + // Derive allowed modes by capability: + // auto -> single + burst + auto + // burst -> single + burst + // single -> single only (no selector) + let fireModes = ["single"]; + if (rawFireModes.includes("auto")) { + fireModes = ["single", "burst", "auto"]; + } else if (rawFireModes.includes("burst")) { + fireModes = ["single", "burst"]; + } + + const defaultFireMode = fireModes[0] || "single"; + const allFireModes = Array.isArray(weaponData.fireModes) ? weaponData.fireModes : ["single"]; + const dialogShotOptions = _getShotOptions(allFireModes); + + const dialogResult = await YZEDiceDialog.show( + baseDice, + `${item.name} Attack`, + `${attrLabel} ${attrValue} + ${skillLabel} ${skillValue} + Gear ${gearBonus}`, + { + returnDetails: true, + fireModes: fireModes.length > 1 ? fireModes : [], + ammoTypes: ["standard", "ap", "he"], + defaultFireMode: defaultFireMode, + defaultAmmoType: weaponData.roundType || weaponData.ammoType || "standard", + modeBonusMap: { single: 0, burst: 1, auto: 2 }, + ammoBonusMap: { standard: 0, ap: 1, he: 0 }, + showAttackMods: true, + shotOptions: dialogShotOptions, + actorId: this.actor?.id, + actorName: this.actor?.name + } + ); + + if (!dialogResult) return; + // Apply multi-shot results from dialog + if (dialogResult.shotsChosen && dialogResult.shotsChosen > 1) { + shotsChosen = dialogResult.shotsChosen; + shotBonus = dialogResult.shotDiceBonus || 0; + baseDice += shotBonus; + } + fireMode = dialogResult.fireMode || "single"; + ammoType = dialogResult.ammoType || "standard"; + const modifier = parseInt(dialogResult.modifier) || 0; + const rangeBonus = parseInt(dialogResult.rangeBonus) || 0; + const scopeBonus = parseInt(dialogResult.scopeBonus) || 0; + const aiBonus = parseInt(dialogResult.aiBonus) || 0; + baseDice = Math.max(0, baseDice + modifier); + baseDice = Math.max(0, baseDice + rangeBonus + scopeBonus + aiBonus); + } + + if (fireMode === "burst") { + modeBonusDice = 1; + modeDamageBonus = 1; + ammoSpent = Math.max(3, shotsChosen); + } else if (fireMode === "auto") { + modeBonusDice = 2; + modeDamageBonus = 2; + ammoSpent = shotsChosen > 1 ? shotsChosen : (parseInt(weaponData.autoAmmoUse) || 8); + } else { + // Single/semi: ammo = shotsChosen (multi-shot dialog already set this) + ammoSpent = shotsChosen; + } + + if (ammoType === "ap") { + ammoBonusDice = 1; + } else if (ammoType === "he") { + ammoDamageBonus = 1; + } + + const totalDice = baseDice + modeBonusDice + ammoBonusDice; + + const label = `${item.name} Attack`; + const formula = `${attrLabel} ${attrValue} + ${skillLabel} ${skillValue} + Gear ${gearBonus}` + + `${armorMods.statMod !== 0 ? ` + Armor Stat ${armorMods.statMod}` : ''}` + + `${armorMods.skillMod !== 0 ? ` + Armor Skill ${armorMods.skillMod}` : ''}` + + `${drugMods.statMod !== 0 ? ` + Drug Stat ${drugMods.statMod}` : ''}` + + `${drugMods.skillMod !== 0 ? ` + Drug Skill ${drugMods.skillMod}` : ''}` + + `${racialMods.bonus !== 0 ? ` + Racial/Drug ${racialMods.bonus > 0 ? '+' : ''}${racialMods.bonus}` : ''}` + + `${condMods.bonus !== 0 ? ` + Conditions ${condMods.bonus}` : ''}` + + `${shotBonus > 0 ? ` + Multi-shot ×${shotsChosen} +${shotBonus}` : ''}` + + `${modeBonusDice > 0 ? ` + Mode ${modeBonusDice}` : ''}` + + `${ammoBonusDice > 0 ? ` + Ammo ${ammoBonusDice}` : ''}` + + `${rangeBonus !== 0 ? ` + Range ${rangeBonus}` : ''}` + + `${scopeBonus > 0 ? ` + Scope ${scopeBonus}` : ''}` + + `${aiBonus > 0 ? ` + AI ${aiBonus}` : ''}` + + ` = ${totalDice}d6`; + + // ── BULLET TAX: deduct credits for rounds fired (no ammo count tracking) ─ + if (!isMelee) { + const taxPaid = await _applyBulletTax(this.actor, weaponData, ammoSpent); + if (taxPaid > 0) { + const cpr = _getAmmoCostForWeapon(weaponData); + ui.notifications.warn( + `💸 Bullet Tax: ${ammoSpent} × ${cpr}c (×2) = −${taxPaid}c charged to ${this.actor.name}` + ); + } + } + + // Weapon info to embed in the roll + const apBase = parseInt(weaponData.ap) || 0; + // Apply round-type AP bonus from ammo catalog + const ammoMods = _getAmmoModifiers(weaponData); + const apEffective = apBase + ammoMods.apBonus + (ammoType === "ap" ? 1 : 0); + const weaponInfo = { + itemId: item.id, + name: item.name, + damage: (parseInt(weaponData.damage) || 0) + ammoMods.damageMod, + ap: apBase, + apEffective: apEffective, + range: weaponData.range || "—", + rof: weaponData.rof || "—", + category: isMelee ? "Melee" : "Ranged", + weaponType: weaponData.weaponType || "", + caliber: weaponData.caliber || SLA_WEAPON_DEFAULT_CALIBER[weaponData.weaponType] || "", + roundType: weaponData.roundType || "standard", + fireMode: fireMode, + ammoType: ammoType, + ammoSpent: isMelee ? 0 : ammoSpent, + bulletTaxPaid: isMelee ? 0 : (ammoSpent * _getAmmoCostForWeapon(weaponData) * 2), + modeBonusDice: modeBonusDice, + ammoBonusDice: ammoBonusDice, + modeDamageBonus: modeDamageBonus, + ammoDamageBonus: ammoDamageBonus, + rangeBonus: rangeBonus, + scopeBonus: scopeBonus, + aiBonus: aiBonus + }; + + return this._rollYZEDice(totalDice, label, formula, attribute, weaponInfo, { skipDialog: !isMelee, preModifiedDice: totalDice }); + } + + /** + * Handle weapon reload button + * Restores ammo to magazine capacity + * @param {Event} event + * @private + */ + async _onWeaponReload(event) { + event.preventDefault(); + event.stopPropagation(); + + const button = event.currentTarget; + const itemId = button.dataset.itemId; + const item = this.actor.items.get(itemId); + if (!item) return; + + const magazine = parseInt(item.system.magazine) || 0; + const currentAmmo = parseInt(item.system.ammo) || 0; + + if (currentAmmo >= magazine) { + ui.notifications.info(`${item.name} is already fully loaded.`); + return; + } + + await item.update({ 'system.ammo': magazine }); + ui.notifications.info(`${item.name} reloaded! (${magazine}/${magazine})`); + + // Post reload to chat + await ChatMessage.create({ + speaker: ChatMessage.getSpeaker({ actor: this.actor }), + content: `
${this.actor.name} reloads ${item.name} (${currentAmmo} → ${magazine})
` + }); + } + + /** + * Handle REST button click — clears all weapon ammoEmpty lockouts + * @param {Event} event + * @private + */ + async _onRestClick(event) { + event.preventDefault(); + event.stopPropagation(); + + const actor = this.actor; + // Also clear any stale broken flag on rest + if (actor.system?.derivedStats?.broken === true) { + const currentHp = Number( + typeof actor.system.derivedStats.health === "object" + ? actor.system.derivedStats.health?.value + : actor.system.derivedStats.health + ) || 0; + if (currentHp > 0) { + await actor.update({ "system.derivedStats.broken": false }, { _zeDerivedRecalcInternal: true }); + } + } + + // Clear Vevaphon instability on rest + if (actor.system?.vevaphon?.instability > 0) { + await actor.update({ 'system.vevaphon.instability': 0 }); + ui.notifications.info(`${actor.name}'s Instability cleared on rest.`); + } + + const rangedWeapons = actor.items.filter(i => i.type === "weapon" && + i.system.category !== "melee" && + String(i.system.range ?? "").toLowerCase() !== "engaged"); + + const lockedWeapons = rangedWeapons.filter(i => i.system.ammoEmpty); + + if (lockedWeapons.length === 0) { + ui.notifications.info(`${actor.name} rests — all weapons are already loaded.`); + } else { + for (const w of lockedWeapons) { + await w.update({ 'system.ammoEmpty': false }); + } + const names = lockedWeapons.map(w => w.name).join(", "); + ui.notifications.info(`${actor.name} rests — weapons restored: ${names}`); + await ChatMessage.create({ + speaker: ChatMessage.getSpeaker({ actor }), + content: `
+ 🛏 ${actor.name} — RESTING
+ Weapons restored: ${names} +
` + }); + } + } + + // ── VEVAPHON INSTABILITY TABLE ─────────────────────────────────────────────── + // Roll: 1d6 + current instability value. + // Results below 7 mean the Vevaphon held together — no effect. + // Table escalates from cosmetic glitches (7) to instant death (36). + static VEVAPHON_INSTABILITY_TABLE = [ + { min: 7, max: 7, name: "FLICKER", severity: "minor", effect: "Surface ripple. No mechanical effect. The form blurs for a moment." }, + { min: 8, max: 8, name: "SKIN CRAWL", severity: "minor", effect: "-1 die to Persuasion and Insight until end of scene. Something is visibly wrong." }, + { min: 9, max: 9, name: "DRIFT", severity: "minor", effect: "Proportions shift. -1 die to Stealth until rest." }, + { min: 10, max: 10, name: "IDENTITY SLIP", severity: "minor", effect: "Forgets one personal detail. Make Wits roll (1 suc) or -1 die to all social rolls this scene." }, + { min: 11, max: 11, name: "VOICE SHIFT", severity: "minor", effect: "Voice changes register and accent. Communications are misidentified as a different person." }, + { min: 12, max: 12, name: "PAIN SPIKE", severity: "moderate", effect: "Realignment burns. +1 Stress immediately.", autoStress: 1 }, + { min: 13, max: 13, name: "LIMB FAULT", severity: "moderate", effect: "One limb locks in wrong form. -2 dice to all physical rolls using that limb until end of scene." }, + { min: 14, max: 14, name: "FORM BLEED", severity: "moderate", effect: "Active form partially degrades. Lose 1 bonus die from current form until next shift." }, + { min: 15, max: 15, name: "SENSORY OVERLOAD", severity: "moderate", effect: "-2 dice to Observation and all Wits rolls for 1 round as input doubles." }, + { min: 16, max: 16, name: "TISSUE REJECTION", severity: "moderate", effect: "Body fights the morph. Take 1 damage — armour does not apply.", autoDamage: 1 }, + { min: 17, max: 17, name: "THREAT LOCK", severity: "serious", effect: "Fixated on nearest visible threat. Must make Wits (2 suc) to take any other action this round." }, + { min: 18, max: 18, name: "FORM PANIC", severity: "serious", effect: "All morph bonuses collapse for 1 round as the body resets. +1 Stress.", autoStress: 1 }, + { min: 19, max: 19, name: "MEMORY FRACTURE", severity: "serious", effect: "Mission brief lost from memory. Must be told again. -1 die to all rolls until re-briefed." }, + { min: 20, max: 20, name: "PARTIAL COLLAPSE", severity: "serious", effect: "Torso and limbs lose cohesion briefly. Take 1d3 damage, -2 dice to all rolls for 1 stretch.", autoDamageRoll: "1d3" }, + { min: 21, max: 21, name: "IDENTITY BLEED", severity: "serious", effect: "No longer recognises own callsign or squad. -2 dice to all social and coordination rolls until rest." }, + { min: 22, max: 22, name: "UNCONTROLLED SHIFT", severity: "serious", effect: "Involuntary morph to a random form (GM rolls 1d3). +1 Instability immediately.", autoInstability: 1 }, + { min: 23, max: 23, name: "TRAUMA RESPONSE", severity: "serious", effect: "Gain the Shaken condition (-1 die to all rolls).", applyCondition: "sla-shaken" }, + { min: 24, max: 24, name: "PERMANENT SCAR", severity: "critical", effect: "Morph process leaves a permanent deformity. One GM-chosen attribute reduced by 1 until medically treated." }, + { min: 25, max: 25, name: "FULL REVERSION", severity: "critical", effect: "Collapses to base form, all morph bonuses lost. Stunned for 1 round. Instability -1.", autoInstability: -1, revertForm: true }, + { min: 26, max: 26, name: "PSYCHIC SCREAM", severity: "critical", effect: "Burst of biogenetic energy. All within Short range must make Wits (1 suc) save or +1 Stress." }, + { min: 27, max: 27, name: "BODY HORROR", severity: "critical", effect: "The morph becomes grotesquely visible. Allies within Short range must make Resolve save or -1 die for 1 stretch." }, + { min: 28, max: 28, name: "CASCADE SHIFT", severity: "critical", effect: "Rapid involuntary cycling through all forms. 1d3 damage, +1 Instability, -3 dice to all rolls for 1 round.", autoDamageRoll: "1d3", autoInstability: 1 }, + { min: 29, max: 29, name: "DISSOLUTION RISK", severity: "critical", effect: "Body begins to lose coherence. At end of next scene, make Strength (2 suc) or take 1d6 damage." }, + { min: 30, max: 30, name: "IDENTITY DEATH", severity: "catastrophic", effect: "All personal identity lost. Character acts on instinct only — GM controls for 1 full scene." }, + { min: 31, max: 31, name: "ORGAN SHIFT", severity: "catastrophic", effect: "Internal organs begin migrating. 2 damage per round for 1d3 rounds. Only stopped by Healing (3 suc).", autoDamage: 2 }, + { min: 32, max: 32, name: "FULL SYSTEM FAILURE",severity: "catastrophic", effect: "Incapacitated for 1 stretch. All form stat bonuses removed permanently until surgery." }, + { min: 33, max: 33, name: "CATASTROPHIC CASCADE",severity:"catastrophic", effect: "Continuous involuntary form cycling. 1d6 damage per round, unable to act. Requires Healing (3 suc) to stabilise.", autoDamageRoll: "1d6" }, + { min: 34, max: 34, name: "BIOGENETIC DETONATION",severity:"catastrophic",effect: "Explosive biogenetic discharge. 2d6 damage to self, 1d6 to everyone at Engaged range. Armour does not apply.", autoDamageRoll: "2d6" }, + { min: 35, max: 35, name: "TERMINAL DISSOLUTION",severity:"lethal", effect: "Biogenetic matrix cannot maintain coherence. Incapacitated. Death in 1d6 rounds unless Healing (4 suc) succeeds." }, + { min: 36, max: 99, name: "TOTAL COLLAPSE", severity: "lethal", effect: "The Vevaphon's biogenetic matrix fails completely. Instant death. The body dissolves into unstructured tissue." }, + ]; + + /** + * Auto-roll 1d6 + instability and apply instability table result. + * Called automatically after every morph shift. + * @param {number} currentInstability + */ + async _rollVevaphonInstabilityCheck(currentInstability) { + const actor = this.actor; + const roll = new Roll('1d6', {}); + await roll.evaluate(); + const dieVal = roll.dice[0].results[0].result; + const total = dieVal + currentInstability; + + // Apply alien dice skin — force slazero2 system via appearance (bypasses user's saved DSN setting) + for (const term of (roll.dice || [])) { + term.options = term.options || {}; + term.options.appearance = { system: 'slazero2' }; + } + + if (total < 7) { + // No effect — silent unless instability is high enough to be notable + await ChatMessage.create({ + speaker: ChatMessage.getSpeaker({ actor }), + flavor: `🔀 ${actor.name} — Instability Check`, + content: `

Rolled ${dieVal} + Instability ${currentInstability} = ${total}

✓ Held together. No effect.

`, + rolls: [roll] + }); + return; + } + + // Find table entry + const INST = ZeroEngineActorSheet.VEVAPHON_INSTABILITY_TABLE; + const entry = INST.find(e => total >= e.min && total <= e.max) || INST[INST.length - 1]; + + const severityColour = { + minor: '#88aaff', moderate: '#ffcc44', serious: '#ff8844', + critical: '#ff4422', catastrophic: '#cc1111', lethal: '#ff0000' + }[entry.severity] || '#aaa'; + + let content = `
`; + content += `

Rolled ${dieVal} + Instability ${currentInstability} = ${total}

`; + content += `

${entry.name}

`; + content += `

${entry.effect}

`; + + // Auto-apply immediate mechanical effects + const updates = {}; + let notes = []; + + if (entry.autoStress) { + const cur = Number(actor.system.derivedStats?.stress ?? 0); + const upd = this._buildStressUpdate(actor, cur + entry.autoStress); + Object.assign(updates, upd); + notes.push(`+${entry.autoStress} Stress auto-applied`); + } + if (entry.autoDamage) { + const hp = Number(actor.system.derivedStats?.health?.value ?? 0); + const newHp = Math.max(0, hp - entry.autoDamage); + updates['system.derivedStats.health.value'] = newHp; + notes.push(`${entry.autoDamage} damage auto-applied`); + } + if (entry.autoDamageRoll) { + const dmgRoll = new Roll(entry.autoDamageRoll, {}); + await dmgRoll.evaluate(); + const hp = Number(actor.system.derivedStats?.health?.value ?? 0); + const newHp = Math.max(0, hp - dmgRoll.total); + updates['system.derivedStats.health.value'] = newHp; + notes.push(`${dmgRoll.total} damage auto-applied`); + } + if (entry.autoInstability) { + const curInst = Number(actor.system.vevaphon?.instability ?? 0); + const newInst = Math.max(0, Math.min(12, curInst + entry.autoInstability)); + updates['system.vevaphon.instability'] = newInst; + notes.push(`Instability ${entry.autoInstability > 0 ? '+' : ''}${entry.autoInstability} → ${newInst}`); + } + if (entry.revertForm) { + updates['system.vevaphon.activeMorphForm'] = 'none'; + notes.push('Reverted to base form'); + } + if (entry.applyCondition) { + await _applyStatusCondition(actor, entry.applyCondition); + notes.push(`Condition applied: ${entry.applyCondition.replace('sla-','')}`); + } + + if (Object.keys(updates).length > 0) { + await actor.update(updates); + } + if (notes.length > 0) { + content += `

Auto-applied: ${notes.join(' | ')}

`; + } + content += '
'; + + await ChatMessage.create({ + speaker: ChatMessage.getSpeaker({ actor }), + flavor: `🔀 ${actor.name} — Instability Check [${entry.severity.toUpperCase()}]`, + content, + rolls: [roll] + }); + } + + async _onRollVevaphonInstability() { + const instab = Number(this.actor.system.vevaphon?.instability ?? 0); + const actor = this.actor; + + if (instab < 6) { + ui.notifications.info(`${actor.name}'s Instability is below 6 — no effects trigger yet.`); + return; + } + + const MINOR_EFFECTS = [ + "Involuntary surface texturing — skin shifts mid-conversation. Social rolls at -1 die.", + "Limb proportions drift. Fine motor tasks at -1 die until end of scene.", + "Voice shifts register unexpectedly. Communications may be misidentified.", + "Brief facial blurring. Anyone describing the Vevaphon gives contradictory reports.", + "Pain from realignment. Gain +1 Stress.", + "Form flickers. One random skill loses 1 die until the Vevaphon rests." + ]; + + const PANIC_TABLE = [ + { result: "Cascade Shift", effect: "Immediately shifts to a random morph form. Costs 1 additional Instability." }, + { result: "Identity Bleed", effect: "Forgets assigned name and call sign until end of scene. Acts on instinct." }, + { result: "Partial Lock", effect: "One limb freezes in wrong form. -2 dice to all physical rolls using that limb." }, + { result: "Tissue Rejection", effect: "Takes 1d3 damage as the body fights itself. Armour does not apply." }, + { result: "Threat Imprint", effect: "Locks on nearest visible target. Must roll Wits to perform any other action." }, + { result: "Full Reversion", effect: "Collapses to base form, losing all morph bonuses. Recover 1 Instability." } + ]; + + const roll = new Roll('1d6', {}); + await roll.evaluate(); + const result = roll.total; + + let content = '
'; + + if (instab >= 10) { + // Check for Morph Panic (roll vs Resolve) + const panicEntry = PANIC_TABLE[Math.min(result - 1, 5)]; + content += `

⚠ MORPH PANIC CHECK (Instability ${instab}/12)

`; + content += `

Rolled: ${result}

`; + content += `

${panicEntry.result}: ${panicEntry.effect}

`; + if (panicEntry.result === "Tissue Rejection") { + const dmgRoll = new Roll('1d3', {}); + await dmgRoll.evaluate(); + const newHp = Math.max(0, Number(actor.system.derivedStats?.health?.value ?? 0) - dmgRoll.total); + await actor.update({ 'system.derivedStats.health.value': newHp }); + content += `

Auto-applied: ${dmgRoll.total} unarmoured damage.

`; + } else if (panicEntry.result === "Full Reversion") { + const newInstab = Math.max(0, instab - 1); + await actor.update({ 'system.vevaphon.activeMorphForm': 'none', 'system.vevaphon.instability': newInstab }); + content += `

Auto-applied: Reverted to base form. Instability reduced to ${newInstab}.

`; + } else if (panicEntry.result === "Cascade Shift") { + const forms = ['brute', 'stalker', 'raptor']; + const randomForm = forms[Math.floor(Math.random() * forms.length)]; + const newInstab = Math.min(12, instab + 1); + await actor.update({ 'system.vevaphon.activeMorphForm': randomForm, 'system.vevaphon.instability': newInstab }); + content += `

Auto-applied: Randomly shifted to ${randomForm} form. Instability: ${newInstab}/12.

`; + } + } else { + // Minor instability effect + const effect = MINOR_EFFECTS[(result - 1) % MINOR_EFFECTS.length]; + content += `

Minor Instability Effect (Instability ${instab}/12)

`; + content += `

Rolled: ${result}

`; + content += `

${effect}

`; + if (effect.includes('+1 Stress')) { + const curStress = Number(actor.system.derivedStats?.stress ?? 0); + await actor.update(this._buildStressUpdate(actor, curStress + 1)); + } + } + + content += '
'; + + await ChatMessage.create({ + speaker: ChatMessage.getSpeaker({ actor }), + flavor: `🔀 ${actor.name} — Vevaphon Instability`, + content, + rolls: [roll] + }); + } + + /** + * Read a derived stat while supporting scalar and legacy object shapes + * @param {Actor} actor + * @param {string} key + * @returns {number} + * @private + */ + _getDerivedStatValue(actor, key) { + const raw = actor?.system?.derivedStats?.[key]; + const value = Number((typeof raw === "object" ? raw?.value : raw) ?? 0); + return Number.isFinite(value) ? value : 0; + } + + /** + * Read derived stat max for object-shaped values + * @param {Actor} actor + * @param {string} key + * @returns {number|null} + * @private + */ + _getDerivedStatMax(actor, key) { + const raw = actor?.system?.derivedStats?.[key]; + if (!raw || typeof raw !== "object") return null; + const max = Number(raw?.max); + return Number.isFinite(max) ? max : null; + } + + /** + * Build derived stat update payload while preserving existing data shape + * @param {Actor} actor + * @param {string} key + * @param {number} value + * @returns {object} + * @private + */ + _buildDerivedStatUpdate(actor, key, value) { + const nextValue = Number(value) || 0; + const current = actor?.system?.derivedStats?.[key]; + if (current && typeof current === "object") { + return { [`system.derivedStats.${key}.value`]: nextValue }; + } + return { [`system.derivedStats.${key}`]: nextValue }; + } + + /** + * Clamp initiative modifier to allowed range + * @param {number} value + * @returns {number} + * @private + */ + _clampInitiativeMod(value) { + const mod = Number(value) || 0; + return Math.max(-5, Math.min(5, mod)); + } + + /** + * Return active armor items; equipped armor is preferred if present + * @param {Actor} actor + * @returns {Item[]} + * @private + */ + _getActiveArmorItems(actor) { + const armors = (actor?.items || []).filter(i => i.type === "armor"); + return armors.filter(i => i?.system?.equipped === true || i?.system?.isEquipped === true); + } + + /** + * Return drug items currently affecting the actor + * @param {Actor} actor + * @returns {Array<{item: Item, state: "active"|"withdrawal"}>} + * @private + */ + _getCurrentDrugStates(actor) { + const states = []; + for (const item of actor?.items || []) { + if (!(item.type === "equipment" && item.system?.isDrug === true)) continue; + if (item.system?.active) { + states.push({ item, state: "active" }); + continue; + } + if (item.system?.withdrawalActive) { + states.push({ item, state: "withdrawal" }); + } + } + return states; + } + + /** + * Get aggregate drug modifiers for rolls + * @param {Actor} actor + * @param {object} opts + * @param {string} [opts.attribute] + * @param {string} [opts.skill] + * @returns {{statMod:number, skillMod:number, total:number, injuryPenaltyIgnore:number}} + * @private + */ + _getDrugRollModifiers(actor, { attribute = "", skill = "" } = {}) { + const attrKey = String(attribute || "").toLowerCase(); + const skillKey = String(skill || "").toLowerCase(); + const isPhysicalAttribute = attrKey === "strength" || attrKey === "agility"; + + let statMod = 0; + let skillMod = 0; + let injuryPenaltyIgnore = 0; + + for (const state of this._getCurrentDrugStates(actor)) { + const data = state.item.system || {}; + const isWithdrawal = state.state === "withdrawal"; + const statPhysical = Number(isWithdrawal ? data.wdStatPhysicalMod : data.statPhysicalMod) || 0; + const skillAll = Number(isWithdrawal ? data.wdSkillAllMod : data.skillAllMod) || 0; + const skillObj = isWithdrawal ? (data.wdSkillMods || {}) : (data.skillMods || {}); + const specificSkill = Number(skillObj?.[skillKey] || 0); + const injuryIgnore = Number(isWithdrawal ? data.wdInjuryPenaltyIgnore : data.injuryPenaltyIgnore) || 0; + + if (isPhysicalAttribute) statMod += statPhysical; + if (skillKey) { + skillMod += skillAll; + skillMod += specificSkill; + } + injuryPenaltyIgnore += injuryIgnore; + } + + return { statMod, skillMod, total: statMod + skillMod, injuryPenaltyIgnore }; + } + + /** + * Get panic reduction from active drugs + * @param {Actor} actor + * @returns {number} + * @private + */ + _getDrugPanicReduction(actor) { + let reduction = 0; + for (const state of this._getCurrentDrugStates(actor)) { + if (state.state !== "active") continue; + reduction += Number(state.item.system?.panicReduction || 0); + } + return Math.max(0, reduction); + } + + /** + * Get racial dice-pool bonuses for a roll. + * Reads `system.racialBonuses` array from specialty items of category "racial". + * Also applies Frother Drug Dependency penalty (-1 all rolls when no drug active). + * + * @param {Actor} actor + * @param {object} opts + * @param {string} [opts.skill] - skill key being rolled (e.g. "melee") + * @param {string} [opts.attribute] - attribute key being rolled + * @param {string} [opts.weaponType] - weapon type string for weapon-gated bonuses (e.g. "Blade") + * @returns {{ bonus: number, breakdown: string[] }} + */ + _getRacialDiceBonus(actor, { skill = "", attribute = "", weaponType = "" } = {}) { + let bonus = 0; + const breakdown = []; + const sk = String(skill || "").toLowerCase(); + const wt = String(weaponType || "").toLowerCase(); + + for (const item of actor.items) { + if (item.type !== "specialty") continue; + if (item.system?.category !== "racial") continue; + const entries = item.system?.racialBonuses; + if (!Array.isArray(entries)) continue; + for (const entry of entries) { + const entrySkill = String(entry.skill || "").toLowerCase(); + if (entrySkill && sk && entrySkill !== sk) continue; // skill mismatch + const tag = String(entry.weaponTag || "").toLowerCase(); + if (tag) { + if (!wt) continue; // need weapon context + if (!wt.includes(tag) && !tag.includes(wt)) continue; // weapon type mismatch + } + const b = Number(entry.bonus) || 0; + if (b === 0) continue; + bonus += b; + breakdown.push(`${item.name} ${b > 0 ? '+' : ''}${b}`); + } + } + + // Frother Drug Dependency: -1 to ALL rolls when no drug is active + const hasDrugDep = actor.items.some( + i => i.type === "specialty" && i.name === "Drug Dependency" + ); + if (hasDrugDep) { + const drugActive = actor.items.some( + i => (i.system?.isDrug || i.type === "drug") && i.system?.active === true + ); + if (!drugActive) { + bonus -= 1; + breakdown.push("Drug Withdrawal −1"); + } + } + + return { bonus, breakdown }; + } + + /** + * Get aggregate armor roll modifiers for physical rolls + * @param {Actor} actor + * @param {object} opts + * @param {string} [opts.attribute] + * @param {string} [opts.skill] + * @returns {{statMod:number, skillMod:number, total:number}} + * @private + */ + _getArmorRollModifiers(actor, { attribute = "", skill = "" } = {}) { + const attrKey = String(attribute || "").toLowerCase(); + const skillKey = String(skill || "").toLowerCase(); + + let statMod = 0; + let skillMod = 0; + for (const armor of this._getActiveArmorItems(actor)) { + const statTarget = String(armor?.system?.statModTarget || "").toLowerCase(); + const skillTarget = String(armor?.system?.skillModTarget || "").toLowerCase(); + + if (statTarget && statTarget === attrKey) { + statMod += Number(armor?.system?.statMod || 0); + } + if (skillTarget && skillTarget === skillKey) { + skillMod += Number(armor?.system?.skillMod || 0); + } + } + + return { statMod, skillMod, total: statMod + skillMod }; + } + + /** + * Get armor dice/auto values from item, with legacy fallback support + * @param {Item} armorItem + * @returns {{armorDice:number, autoArmor:number}} + * @private + */ + _getArmorProtectionValues(armorItem) { + const system = armorItem?.system || {}; + const legacyArmor = Number(system.armorRating || system.armor || 0); + const armorDice = Math.max(0, Number(system.armorDice ?? legacyArmor) || 0); + const autoArmor = Math.max(0, Number(system.armorAuto ?? 0) || 0); + return { armorDice, autoArmor }; + } + + /** + * Prompt for incoming damage/AP and roll armor check + * @param {Actor} actor + * @returns {Promise} + * @private + */ + async _promptAndRollArmorCheck(actor, preselectedArmorId = null) { + const allArmors = this._getActiveArmorItems(actor); + const preselectedArmor = preselectedArmorId + ? actor.items.get(preselectedArmorId) + : null; + if (preselectedArmor && !(preselectedArmor?.system?.equipped === true || preselectedArmor?.system?.isEquipped === true)) { + ui.notifications.warn(`${preselectedArmor.name} is not equipped.`); + return; + } + const armors = preselectedArmor ? [preselectedArmor] : allArmors; + if (armors.length === 0) { + ui.notifications.warn(`${actor.name} has no armor items.`); + return; + } + + const options = armors.map((a, idx) => { + const prot = this._getArmorProtectionValues(a); + return ``; + }).join(""); + + const config = await new Promise(resolve => { + new Dialog({ + title: `${actor.name} - Armor Check`, + content: ` +
+
+ + +
+
+ + +
+ ${preselectedArmor ? `
${preselectedArmor.name} (${this._getArmorProtectionValues(preselectedArmor).armorDice}(${this._getArmorProtectionValues(preselectedArmor).autoArmor}))
` : `
`} +
+ `, + buttons: { + roll: { + label: "Roll Armor", + callback: html => { + const incomingDamage = Math.max(0, Number(html.find('[name="incomingDamage"]').val()) || 0); + const ap = Math.max(0, Number(html.find('[name="ap"]').val()) || 0); + const armorIndex = preselectedArmor ? 0 : Math.max(0, Number(html.find('[name="armorIndex"]').val()) || 0); + resolve({ incomingDamage, ap, armorItem: armors[armorIndex] }); + } + }, + cancel: { + label: "Cancel", + callback: () => resolve(null) + } + }, + default: "roll", + close: () => resolve(null) + }).render(true); + }); + + if (!config) return; + await this._rollArmorCheck(actor, config); + } + + /** + * Roll armor check and apply resulting health damage + * @param {Actor} actor + * @param {{incomingDamage:number, ap:number, armorItem:Item}} config + * @returns {Promise} + * @private + */ + async _rollArmorCheck(actor, { incomingDamage = 0, ap = 0, armorItem = null } = {}) { + if (!armorItem) return; + + const { armorDice, autoArmor } = this._getArmorProtectionValues(armorItem); + const incoming = Math.max(0, Number(incomingDamage) || 0); + const apValue = Math.max(0, Number(ap) || 0); + const effectiveDice = Math.max(0, armorDice - apValue); + const afterAuto = Math.max(0, incoming - autoArmor); + + let armorSuccesses = 0; + let armorRoll = null; + if (effectiveDice > 0) { + armorRoll = await (new Roll(`${effectiveDice}d6`)).evaluate(); + if (game.dice3d) { + await game.dice3d.showForRoll(armorRoll, game.user, true); + } + armorSuccesses = (armorRoll.dice?.[0]?.results || []).filter(r => r.result === 6).length; + } + + const finalDamage = Math.max(0, afterAuto - armorSuccesses); + const currentHealth = this._getDerivedStatValue(actor, "health"); + const nextHealth = Math.max(0, currentHealth - finalDamage); + const updates = this._buildDerivedStatUpdate(actor, "health", nextHealth); + if (nextHealth <= 0) updates["system.derivedStats.broken"] = true; + if (finalDamage > 0 || nextHealth <= 0) { + await actor.update(updates); + } + + const armorDieFaces = armorRoll + ? (armorRoll.dice?.[0]?.results || []).map(r => + `${r.result}` + ).join('') + : ''; + const armorRollLine = effectiveDice > 0 + ? `Armor roll: ${effectiveDice}d6 ${armorDieFaces} → ${armorSuccesses} success${armorSuccesses !== 1 ? "es" : ""}` + : "Armor roll: 0d6 (no armor dice after AP)"; + + const content = ` +
+
Armor Check - ${armorItem.name}
+
+
Incoming damage: ${incoming}
+
Armor value: ${armorDice}(${autoArmor})
+
AP: ${apValue} (dice reduced to ${effectiveDice})
+
Auto armor: ${autoArmor} (damage after auto: ${afterAuto})
+
${armorRollLine}
+
Final damage: ${finalDamage}
+
Health: ${currentHealth} -> ${nextHealth}
+
+
+ `; + + await ChatMessage.create({ + speaker: ChatMessage.getSpeaker({ actor }), + flavor: `${actor.name} - Armor Check`, + content: content, + rolls: armorRoll ? [armorRoll] : [] + }); + } + + /** + * Read stress from actor data while supporting scalar and legacy object shapes + * @param {Actor} actor + * @returns {number} + * @private + */ + _getStressValue(actor) { + return Math.max(0, this._getDerivedStatValue(actor, "stress")); + } + + /** + * Build stress update payload while preserving existing data shape + * @param {Actor} actor + * @param {number} value + * @returns {object} + * @private + */ + _buildStressUpdate(actor, value) { + return this._buildDerivedStatUpdate(actor, "stress", Math.max(0, Number(value) || 0)); + } + + /** + * Roll Year SLA Zero Two dice with dialog + * @param {number} baseDice - Base number of dice to roll + * @param {string} label - Label for the roll + * @param {string} formula - Formula string to display + * @param {string} attribute - Which attribute (for push cost) + * @param {object|null} weaponInfo - Optional weapon data {name, damage, ap, range, rof, category, weaponType} + * @private + */ + async _rollYZEDice(baseDice, label, formula = '', attribute = '', weaponInfo = null, rollOptions = {}) { + let modifiedDice = baseDice; + if (!rollOptions.skipDialog) { + // Show dialog to modify dice pool + modifiedDice = await YZEDiceDialog.show(baseDice, label, formula, { + actorId: this.actor?.id, + actorName: this.actor?.name + }); + if (modifiedDice === null) return; // Dialog was cancelled + } else if (typeof rollOptions.preModifiedDice === "number") { + modifiedDice = rollOptions.preModifiedDice; + } + + const normalDice = Math.max(0, parseInt(modifiedDice) || 0); + const stressDice = this._getStressValue(this.actor); + + // Roll normal + stress dice together (stress dice are always the last term) + const stressTerm = stressDice > 0 ? ` + ${stressDice}d6` : ''; + const roll = new Roll(`${normalDice}d6${stressTerm}`, {}); + await roll.evaluate(); + + // Show Dice So Nice animation if available + if (game.dice3d) { + this._applyDiceSoNiceColors(roll, normalDice, stressDice); + await game.dice3d.showForRoll(roll, game.user, true); + } + + const ebbInfo = rollOptions.ebbInfo || null; + + const { normalResults, stressResults } = this._splitRollResultsByType(roll, { + normalDice, + stressDice + }); + const dice = this._buildDiceArray(normalResults, stressResults); + const successes = dice.filter(d => d.result === 6).length; + const stressBanes = dice.filter(d => d.type === 'stress' && d.result === 1).length; + + // Auto-apply ebb mechanical effects where possible + if (ebbInfo) { + await this._applyEbbEffect(ebbInfo, successes); + } + + // Create custom chat message with YZE formatting + const stressLabel = stressDice > 0 ? ` + Stress ${stressDice}` : ''; + const stressTag = stressDice > 0 ? ` Stress: ${stressDice}` : ''; + const formulaDisplay = formula ? `${formula}${stressLabel}${stressTag}` : `Rolling ${normalDice}d6${stressLabel}${stressTag}`; + + const _speaker = ChatMessage.getSpeaker({ actor: this.actor }); + const chatData = { + speaker: _speaker, + flavor: `${label}
${formulaDisplay}`, + content: await this._formatYZEResult(dice, successes, stressBanes, false, weaponInfo, ebbInfo), + style: CONST.CHAT_MESSAGE_STYLES?.ROLL, + type: CONST.CHAT_MESSAGE_TYPES?.ROLL, + roll: roll, + rollMode: game.settings.get('core', 'rollMode'), + flags: { + 'slazero2': { + canPush: dice.some(d => d.result !== 6), + pushed: false, + normalResults: normalResults, + stressResults: stressResults, + normalDiceCount: normalDice, + stressDiceCount: stressDice, + originalSuccesses: successes, + isAttack: !!weaponInfo, + attackCategory: weaponInfo?.category || null, + attribute: attribute, + actorId: this.actor.id, + tokenId: _speaker.token ?? null, + sceneId: _speaker.scene ?? null, + weaponInfo: weaponInfo, + ebbInfo: ebbInfo + } + } + }; + + return ChatMessage.create(chatData); + } + + /** + * Format YZE roll result for chat + * @param {Array} dice - Array of dice results + * @param {number} successes - Number of successes (6s) + * @param {number} stressBanes - Number of stress banes (1s on stress dice) + * @param {boolean} pushed - Whether this is a pushed result + * @param {object|null} weaponInfo - Optional weapon data {name, damage, ap, range, rof, category, weaponType} + * @private + */ + async _formatYZEResult(dice, successes, stressBanes, pushed = false, weaponInfo = null, ebbInfo = null) { + let html = '
'; + + if (pushed) { + html += '
⚡ PUSHED RESULT
'; + } + if (pushed && stressBanes > 0) { + html += '
PANIC CHECK!
'; + } + + // Show individual dice + const stressDiceCount = dice.filter(d => d.type === 'stress').length; + const normalDice = dice.filter(d => d.type !== 'stress'); + const stressDice = dice.filter(d => d.type === 'stress'); + + html += `
`; + normalDice.forEach(d => { + const value = d.result; + const dieType = d.type || 'normal'; + let diceClass = 'yze-die'; + if (dieType === 'stress') diceClass += ' yze-stress'; + if (d.locked) diceClass += ' yze-locked'; + if (value === 6) diceClass += ' yze-success'; + html += `
${value}
`; + }); + + if (stressDiceCount > 0) { + html += '
'; + html += '
STRESS DICE
'; + stressDice.forEach(d => { + const value = d.result; + const dieType = d.type || 'stress'; + let diceClass = 'yze-die'; + if (dieType === 'stress') diceClass += ' yze-stress'; + if (d.locked) diceClass += ' yze-locked'; + if (value === 6) diceClass += ' yze-success'; + else if (value === 1) diceClass += ' yze-bane'; + html += `
${value}
`; + }); + } + html += '
'; + + // Summary - always show full breakdown on pushed results + html += '
'; + html += `${successes} Success${successes !== 1 ? 'es' : ''}`; + if (pushed || stressBanes > 0) { + html += ` | ${stressBanes} Stress Bane${stressBanes !== 1 ? 's' : ''}`; + if (!pushed && stressBanes > 0) { + html += ` (no effect unless pushed or GM calls Panic)`; + } + } + html += '
'; + + // Weapon damage section (shown when this is a weapon attack roll) + if (weaponInfo) { + const extraSuccesses = Math.max(0, successes - 1); // 1 success is spent to hit + const modeDamageBonus = weaponInfo.modeDamageBonus || 0; + const ammoDamageBonus = weaponInfo.ammoDamageBonus || 0; + const totalDamage = weaponInfo.damage + extraSuccesses + modeDamageBonus + ammoDamageBonus; + html += '
'; + html += `
${weaponInfo.name}
`; + html += '
'; + if (successes > 0) { + html += `
Total Damage${totalDamage}
`; + html += `
(Base ${weaponInfo.damage}` + + `${extraSuccesses > 0 ? ` + ${extraSuccesses} extra success${extraSuccesses !== 1 ? 'es' : ''}` : ''}` + + `${modeDamageBonus > 0 ? ` + ${modeDamageBonus} mode` : ''}` + + `${ammoDamageBonus > 0 ? ` + ${ammoDamageBonus} ammo` : ''}` + + `)
`; + } else { + html += `
MISS
`; + } + if (weaponInfo.fireMode || weaponInfo.ammoType) { + html += `
Mode: ${weaponInfo.fireMode || 'single'} | Ammo: ${weaponInfo.ammoType || 'standard'}
`; + } + if (weaponInfo.ammoSpent) { + const calLabel = weaponInfo.caliber ? ` [${weaponInfo.caliber} ${weaponInfo.roundType || 'std'}]` : ''; + const taxLine = weaponInfo.bulletTaxPaid > 0 + ? `💸 Bullet Tax: −${weaponInfo.bulletTaxPaid}c` + : ''; + html += `
Rounds fired: ${weaponInfo.ammoSpent}${calLabel}${taxLine ? ' · ' + taxLine : ''}
`; + } + html += `
`; + html += `AP ${weaponInfo.apEffective ?? weaponInfo.ap}`; + html += `Range ${weaponInfo.range}`; + html += `ROF ${weaponInfo.rof}`; + html += `Type ${weaponInfo.category}`; + if (weaponInfo.category === 'Ranged' && weaponInfo.magazine > 0) { + html += `Ammo ${weaponInfo.ammo}/${weaponInfo.magazine}`; + } + html += `
`; + html += '
'; + html += '
'; + } + + // EBB formula outcome block + if (ebbInfo) { + const ebbSucceeded = successes >= ebbInfo.requiredSuccesses; + const discLabel = ebbInfo.discipline + ? ebbInfo.discipline.charAt(0).toUpperCase() + ebbInfo.discipline.slice(1) + : "Ebb"; + html += `
`; + html += `
`; + html += `${discLabel}`; + if (ebbSucceeded) { + html += `✓ ACTIVATED`; + } else { + html += `✗ FAILED — need ${ebbInfo.requiredSuccesses} success${ebbInfo.requiredSuccesses !== 1 ? 'es' : ''}`; + } + html += `
`; + if (ebbSucceeded && ebbInfo.effect) { + html += `
${ebbInfo.effect}
`; + if (ebbInfo.range || ebbInfo.duration) { + html += `
`; + if (ebbInfo.range) html += `Range ${ebbInfo.range} `; + if (ebbInfo.duration) html += `Duration ${ebbInfo.duration}`; + html += `
`; + } + } else if (!ebbSucceeded) { + html += `
Formula failed to activate. Flux was spent.
`; + } + if (pushed && stressBanes > 0 && ebbInfo.catastrophe) { + html += `
⚠ CATASTROPHE: ${ebbInfo.catastrophe}
`; + } + html += `
`; + } + + // Push button (only shown if not pushed yet and not all 6s) + if (!pushed && dice.some(d => d.result !== 6)) { + html += '
'; + html += ''; + html += '
Keep 6s. Re-roll others. Stress 1s are locked. +1 Stress die on push.
'; + html += '
'; + } + + html += '
'; + return html; + } + + /** + * Handle Push button clicks from chat + * @param {ChatMessage} message - The chat message containing the roll + * @private + */ + async _onPushRoll(message) { + const flags = message.flags['slazero2']; + if (!flags || flags.pushed) { + ui.notifications.warn("This roll has already been pushed!"); + return; + } + + if (!flags.canPush) { + ui.notifications.warn("This roll cannot be pushed!"); + return; + } + + // Resolve the correct actor — prefer the token actor (unlinked tokens have + // separate data from the base world actor and must be updated via the token doc). + let actor = null; + if (flags.tokenId && flags.sceneId) { + const scene = game.scenes?.get(flags.sceneId); + const tokenDoc = scene?.tokens?.get(flags.tokenId); + if (tokenDoc) actor = tokenDoc.actor; + } + if (!actor) actor = game.actors.get(flags.actorId); + if (!actor) { + ui.notifications.error("Actor not found!"); + return; + } + + const currentStress = this._getStressValue(actor); + const updatedStress = currentStress + 1; + + // Get original dice results by type (fallback to legacy diceResults as normal dice) + const originalNormal = Array.isArray(flags.normalResults) ? flags.normalResults : (Array.isArray(flags.diceResults) ? flags.diceResults : []); + const originalStress = Array.isArray(flags.stressResults) ? flags.stressResults : []; + + // Lock 6s for all dice. Lock 1s for stress dice. + const lockedNormalSixes = originalNormal.filter(d => d === 6); + const lockedStressSixes = originalStress.filter(d => d === 6); + const lockedStressOnes = originalStress.filter(d => d === 1); + const normalRerollCount = originalNormal.filter(d => d !== 6).length; + const stressRerollCount = originalStress.filter(d => d !== 6 && d !== 1).length; + const addedStressDice = 1; // pushing adds +1 Stress die + + if ((normalRerollCount + stressRerollCount + addedStressDice) === 0) { + ui.notifications.warn("All dice are 6s - nothing to re-roll!"); + return; + } + + // Roll new dice for non-locked results (normal + stress) + const normalTerm = normalRerollCount > 0 ? `${normalRerollCount}d6` : ''; + const stressTerm = (stressRerollCount + addedStressDice) > 0 ? `${stressRerollCount + addedStressDice}d6` : ''; + const rerollFormula = [normalTerm, stressTerm].filter(Boolean).join(' + '); + const reroll = new Roll(rerollFormula, {}); + await reroll.evaluate(); + + // Show Dice So Nice animation if available + if (game.dice3d) { + this._applyDiceSoNiceColors(reroll, normalRerollCount, stressRerollCount + addedStressDice); + await game.dice3d.showForRoll(reroll, game.user, true); + } + + // Combine locked dice with new results + const rerollResults = this._splitRollResultsByType(reroll, { + normalDice: normalRerollCount, + stressDice: stressRerollCount + addedStressDice + }); + const newNormalResults = [...lockedNormalSixes, ...rerollResults.normalResults]; + const newStressResults = [...lockedStressSixes, ...lockedStressOnes, ...rerollResults.stressResults]; + const newDice = [ + ...newNormalResults.map(result => ({ result, type: 'normal' })), + ...lockedStressOnes.map(result => ({ result, type: 'stress', locked: true })), + ...lockedStressSixes.map(result => ({ result, type: 'stress' })), + ...rerollResults.stressResults.map(result => ({ result, type: 'stress' })) + ]; + const successes = newDice.filter(d => d.result === 6).length; + const stressBanes = newDice.filter(d => d.type === 'stress' && d.result === 1).length; + const hasStressBane = stressBanes > 0; + + // Carry forward weaponInfo and ebbInfo if present + const weaponInfo = flags.weaponInfo || null; + const ebbInfo = flags.ebbInfo || null; + + // ── AMMO LOCKOUT: if a ranged weapon was pushed and a stress die shows 1, lock the weapon ─ + if (weaponInfo && weaponInfo.category === "Ranged" && weaponInfo.itemId && stressBanes > 0) { + const weaponItem = actor.items.get(weaponInfo.itemId); + if (weaponItem) { + await weaponItem.update({ 'system.ammoEmpty': true }); + await ChatMessage.create({ + speaker: ChatMessage.getSpeaker({ actor }), + content: `
+ 🔒 ${weaponInfo.name} — OUT OF AMMO
+ A stress die came up 1 on the push — the weapon has run dry. Rest to restore ammunition. +
` + }); + } + } + + // Pushing adds +1 Stress immediately + await actor.update(this._buildStressUpdate(actor, updatedStress)); + const stressFlavor = `Stress increased: ${currentStress} → ${updatedStress} | +1 Stress die added`; + + const originalSuccesses = parseInt(flags.originalSuccesses) || 0; + const totalSuccesses = originalSuccesses + successes; + + // Create pushed result chat card + const chatData = { + speaker: ChatMessage.getSpeaker({ actor }), + flavor: `⚡ PUSHED ROLL
${stressFlavor}`, + content: await this._formatYZEResult(newDice, successes, stressBanes, true, weaponInfo, ebbInfo), + flags: { + 'slazero2': { + pushed: true, + canPush: false, + normalResults: newNormalResults, + stressResults: newStressResults, + actorId: actor.id, + hasStressBanes: hasStressBane, + originalSuccesses: originalSuccesses, + totalSuccesses: totalSuccesses, + weaponInfo: weaponInfo, + ebbInfo: ebbInfo + } + } + }; + + await ChatMessage.create(chatData); + + // Trigger Panic or Critical Failure if stress bane present (including locked 1s from initial roll) + if (hasStressBane) { + const isAttack = !!flags.isAttack; + const attackCategory = (flags.attackCategory || weaponInfo?.category || '').toLowerCase(); + const isRanged = attackCategory === 'ranged'; + + if (isAttack && totalSuccesses === 0) { + ui.notifications.warn(`${actor.name} suffers a CRITICAL FAILURE!`); + await this._rollWeaponCriticalFail(actor, isRanged); + } else { + ui.notifications.info(`${actor.name} rolled a Stress bane - Panic check triggered!`); + await this._rollPanic(actor); + } + } + } + + /** + * Apply self-only panic consequences to actor resources + * @param {Actor} actor + * @param {object} panicEffect + * @returns {Promise} + * @private + */ + async _applyPanicConsequences(actor, panicEffect) { + const changes = panicEffect?.selfChanges || {}; + const labels = { + health: "Health", + resolve: "Resolve", + stress: "Stress" + }; + + const updates = {}; + const applied = []; + + for (const key of ["health", "resolve", "stress"]) { + const delta = Number(changes[key] || 0); + if (!Number.isFinite(delta) || delta === 0) continue; + + const current = key === "stress" + ? this._getStressValue(actor) + : this._getDerivedStatValue(actor, key); + + let next = current + delta; + if (key === "stress") { + next = Math.max(0, next); + } else { + const max = this._getDerivedStatMax(actor, key); + next = Math.max(0, next); + if (max !== null) next = Math.min(next, max); + } + + if (next === current) continue; + + Object.assign(updates, this._buildDerivedStatUpdate(actor, key, next)); + applied.push(`${labels[key]} ${current} -> ${next} (${delta > 0 ? "+" : ""}${delta})`); + } + + if (Object.keys(updates).length > 0) { + await actor.update(updates); + } + + return applied; + } + + /** + * Roll Panic check (1d6 + Stress) + * @param {Actor} actor - The actor making the panic roll + * @private + */ + async _rollPanic(actor) { + const stress = this._getStressValue(actor); + + // Roll 1d6 + Stress + const roll = new Roll(`1d6 + ${stress}`, {}); + await roll.evaluate(); + + // Apply alien dice skin — force slazero2 system via appearance + for (const term of (roll.dice || [])) { + term.options = term.options || {}; + term.options.appearance = { system: "slazero2" }; + } + + const rawTotal = roll.total; + const dieVal = roll.dice[0].results[0].result; + const panicReduction = this._getDrugPanicReduction(actor); + const total = panicReduction > 0 ? Math.max(7, rawTotal - panicReduction) : rawTotal; + + // Create panic result chat card + let content = '
'; + content += `

Rolled: ${dieVal} + ${stress} Stress = ${rawTotal}

`; + if (panicReduction > 0) { + content += `

Drug Panic Reduction: -${panicReduction} => ${total}

`; + } + + if (total <= 6) { + content += '
✓ Held it together! (No panic effect)
'; + } else { + const panicEffect = this._getPanicEffect(total); + const appliedChanges = await this._applyPanicConsequences(actor, panicEffect); + + content += `
${panicEffect.name}: ${panicEffect.text}
`; + if (appliedChanges.length > 0) { + content += `
Auto-applied: ${appliedChanges.join(' | ')}
`; + } + + if (panicEffect?.autoMentalCritical) { + content += '
Auto: Rolling Mental Critical Injury.
'; + } + } + + content += '
'; + + ChatMessage.create({ + speaker: ChatMessage.getSpeaker({ actor }), + content: content, + rolls: [roll] + }); + + // Apply condition based on panic severity + const panicLevel = total || 0; + if (panicLevel >= 10) { + await _applyStatusCondition(actor, 'sla-panicking', 'Panicking', 'icons/svg/terror.svg'); + ui.notifications.warn(`${actor.name} is PANICKING! (-2 dice to all rolls)`); + } else if (panicLevel >= 7) { + await _applyStatusCondition(actor, 'sla-shaken', 'Shaken', 'icons/svg/terror.svg'); + ui.notifications.warn(`${actor.name} is SHAKEN! (-1 die to all rolls)`); + } + + if (total > 6) { + const panicEffect = this._getPanicEffect(total); + if (panicEffect?.autoMentalCritical) { + await this._rollMentalCritical(actor); + } + } + + } + async _rollInitiative(useMobility = true, preferredCombatantId = null, preferredCombatId = null) { + // ── Guard: prevent double-calls within 2 seconds (DSN/tracker re-render race) ─ + const lockKey = `_initLock_${this.actor?.id}`; + if (window[lockKey]) return; + window[lockKey] = true; + setTimeout(() => { delete window[lockKey]; }, 2000); + + const actor = this.actor; + const system = actor.system; + + // ── Mode: AGI + Mobility OR WIT + Observation ────────────────────────── + const attrKey = useMobility ? "agility" : "wits"; + const skillKey = useMobility ? "mobility" : "observation"; + const modeLabel = useMobility ? "AGI + Mobility" : "WIT + Observation"; + + const attrVal = parseInt(system.attributes?.[attrKey]?.value) || 0; + const skillVal = parseInt(system.skills?.[skillKey]?.value) || 0; + + // Base score is fixed — attribute + skill value + const baseScore = attrVal + skillVal; + + // ── Condition penalties reduce the roll pool (conditions affect all rolls) ─ + const initCondMods = _getConditionModifiers(actor, { attribute: attrKey, skill: skillKey }); + + // ── Roll the same pool for the bonus successes (no stress dice) ────────── + const pool = Math.max(1, baseScore + initCondMods.bonus); + const roll = new Roll(`${pool}d6`); + await roll.evaluate(); + + if (game.dice3d) { + await game.dice3d.showForRoll(roll, game.user, true); + } + + const rollSuccesses = roll.dice[0].results.filter(r => r.result === 6).length; + + // ── Specialty bonuses ──────────────────────────────────────────────────── + let specialtyBonus = 0; + const specialtyBreakdown = []; + + for (const item of actor.items) { + if (item.type !== "specialty") continue; + // Fast Reflexes: +2 to initiative + if (item.name === "Fast Reflexes") { + specialtyBonus += 2; + specialtyBreakdown.push("Fast Reflexes +2"); + continue; + } + // Any specialty with an explicit initiativeBonus field + const bonus = parseInt(item.system?.initiativeBonus) || 0; + if (bonus !== 0) { + specialtyBonus += bonus; + specialtyBreakdown.push(`${item.name} ${bonus > 0 ? '+' : ''}${bonus}`); + } + } + + // ── Gear modifiers (equipped items with initiativeMod) ─────────────────── + let gearBonus = 0; + const gearBreakdown = []; + for (const item of actor.items) { + const mod = this._clampInitiativeMod(item?.system?.initiativeMod); + if (mod === 0) continue; + const isEquipped = item?.system?.equipped === true || item?.system?.isEquipped === true; + if (!isEquipped) continue; + gearBonus += mod; + gearBreakdown.push(`${item.name} ${mod > 0 ? '+' : ''}${mod}`); + } + + // ── Active effect modifiers ────────────────────────────────────────────── + for (const e of (actor.effects || [])) { + const effMod = e.getFlag('slazero2', 'initiativeMod'); + if (effMod) { + const em = Number(effMod) || 0; + if (em !== 0) { + gearBonus += em; + gearBreakdown.push(`${e.name} ${em > 0 ? '+' : ''}${em}`); + } + } + } + + // ── Drug modifiers ─────────────────────────────────────────────────────── + const drugMods = this._getDrugRollModifiers(actor, { attribute: attrKey, skill: skillKey }); + if (drugMods.total !== 0) { + gearBonus += drugMods.total; + gearBreakdown.push(`Drugs ${drugMods.total > 0 ? '+' : ''}${drugMods.total}`); + } + + // ── Final initiative = base + roll successes + bonuses ─────────────────── + const finalInit = baseScore + rollSuccesses + specialtyBonus + gearBonus; + + // ── Tie-breaker d6 (silent — no Roll object so Dice So Nice doesn't animate it) ─ + const tieVal = Math.floor(Math.random() * 6) + 1; + + // ── Store on actor + push to combat tracker (single write, no yze flags) ─ + await actor.update({ "system.combat.initiative": finalInit }); + + let combat = (preferredCombatId ? game.combats?.get(preferredCombatId) : null) || game.combat || null; + let combatant = null; + + if (preferredCombatantId && combat) { + combatant = combat.combatants.get(preferredCombatantId) || null; + } + if (!combatant) { + const ctx = await this._ensureActorInEncounter(actor, preferredCombatId); + combat = ctx?.combat || combat; + combatant = ctx?.combatant || (combat?.getCombatantByActor(actor.id) ?? null); + } + if (combatant) { + // Single update: set initiative + sync yze-combat display value in one write + await combatant.update({ + initiative: finalInit, + "flags.yze-combat.cardValue": finalInit + }); + } + if (combat) { + if (typeof combat.setupTurns === "function") combat.setupTurns(); + ui.combat?.render(false); + } + + // ── Chat card ──────────────────────────────────────────────────────────── + const diceHTML = roll.dice[0].results.map(r => + `${r.result}` + ).join(''); + + const attrLabel = attrKey.slice(0, 3).toUpperCase(); + const skillLabel = skillKey.charAt(0).toUpperCase() + skillKey.slice(1); + + const totalBonus = specialtyBonus + gearBonus; + const bonusLine = [...specialtyBreakdown, ...gearBreakdown].join(' · ') || '—'; + + const content = ` +
+
⚡ Initiative — ${modeLabel}
+
+ Base: ${attrLabel} ${attrVal} + ${skillLabel} ${skillVal} = ${baseScore} +
+
Roll (${pool}d6): ${diceHTML}
+
Successes: +${rollSuccesses}
+ ${totalBonus !== 0 ? `
Bonuses: ${totalBonus > 0 ? '+' : ''}${totalBonus} (${bonusLine})
` : ''} + ${initCondMods.bonus !== 0 ? `
Conditions: ${initCondMods.bonus} (${initCondMods.breakdown.join(', ')})
` : ''} +
Final Initiative: ${finalInit}
+
Tie-breaker d6: ${tieVal}
+
`; + + // No rolls:[roll] here — the card already shows individual die faces, + // and omitting it prevents DSN from showing the dice a second time via + // the chat message creation hook. + await ChatMessage.create({ + speaker: ChatMessage.getSpeaker({ actor }), + flavor: `${actor.name} — Initiative`, + content + }); + + return { roll, finalInit, tieVal }; + } + + /** + * Ensure actor is present in the current scene encounter if a token is available. + * If there is no encounter on scene, create one. + * @param {Actor} actor + * @returns {Promise<{combat: Combat|null, combatant: Combatant|null}>} + * @private + */ + async _ensureActorInEncounter(actor, preferredCombatId = null) { + const scene = game.scenes?.current || canvas?.scene; + if (!scene) return { combat: game.combat || null, combatant: game.combat?.getCombatantByActor(actor.id) || null }; + + let tokenDoc = null; + const controlled = canvas?.tokens?.controlled?.find(t => t.actor?.id === actor.id); + if (controlled?.document) { + tokenDoc = controlled.document; + } else { + const placeable = canvas?.tokens?.placeables?.find(t => t.actor?.id === actor.id); + tokenDoc = placeable?.document || null; + } + if (!tokenDoc) { + return { combat: game.combat || null, combatant: game.combat?.getCombatantByActor(actor.id) || null }; + } + + let combat = preferredCombatId ? (game.combats?.get(preferredCombatId) || null) : null; + if (!combat) combat = game.combat; + if (!combat || combat.scene?.id !== scene.id) { + combat = game.combats?.find(c => c.scene?.id === scene.id && c.active) + || game.combats?.find(c => c.scene?.id === scene.id) + || null; + } + if (!combat) { + combat = await Combat.create({ scene: scene.id, active: true }); + } + + let combatant = combat.getCombatantByToken(tokenDoc.id) || combat.getCombatantByActor(actor.id); + if (!combatant) { + const actorInit = Number(actor.system?.combat?.initiative ?? 0) || 0; + const created = await combat.createEmbeddedDocuments("Combatant", [{ + tokenId: tokenDoc.id, + actorId: actor.id, + hidden: tokenDoc.hidden === true, + initiative: actorInit, + flags: { + "yze-combat": { + cardValue: actorInit, + cardName: "INIT" + } + } + }]); + combatant = created?.[0] || combat.getCombatantByToken(tokenDoc.id) || null; + } + + return { combat, combatant }; + } + + + /** + * Roll critical failure table for weapon attacks + * @param {Actor} actor + * @param {boolean} isRanged + * @private + */ + async _rollWeaponCriticalFail(actor, isRanged = true) { + const tableName = isRanged + ? "Ranged Weapon Critical Fail (2-12)" + : "Melee Weapon Critical Fail (2-12)"; + + const table = game.tables.getName(tableName); + if (!table) { + ui.notifications.warn(`Critical fail table not found: ${tableName}`); + return; + } + + const roll = await (new Roll("2d6")).evaluate(); + if (game.dice3d) { + await game.dice3d.showForRoll(roll, game.user, true); + } + await table.draw({ roll, displayChat: true }); + } + + /** + * Split roll results by normal vs stress dice counts + * @param {Roll} roll + * @param {object} counts + * @param {number} counts.normalDice + * @param {number} counts.stressDice + * @returns {{normalResults: number[], stressResults: number[]}} + * @private + */ + _splitRollResultsByType(roll, { normalDice = 0, stressDice = 0 }) { + const diceTerms = roll.dice || []; + let normalResults = []; + let stressResults = []; + + if (normalDice > 0 && stressDice > 0) { + normalResults = (diceTerms[0]?.results || []).map(r => r.result); + stressResults = (diceTerms[1]?.results || []).map(r => r.result); + } else if (normalDice > 0) { + normalResults = (diceTerms[0]?.results || []).map(r => r.result); + } else if (stressDice > 0) { + stressResults = (diceTerms[0]?.results || []).map(r => r.result); + } + + return { normalResults, stressResults }; + } + + /** + * Build a unified dice array with type annotations for chat rendering + * @param {number[]} normalResults + * @param {number[]} stressResults + * @returns {{result:number, type:string}[]} + * @private + */ + _buildDiceArray(normalResults = [], stressResults = []) { + return [ + ...normalResults.map(result => ({ result, type: 'normal' })), + ...stressResults.map(result => ({ result, type: 'stress' })) + ]; + } + + /** + * Build Dice So Nice options to color stress dice + * @param {Roll} roll + * @param {number} normalDice + * @param {number} stressDice + * @returns {object} + * @private + */ + _applyDiceSoNiceColors(roll, normalDice = 0, stressDice = 0) { + const diceTerms = roll.dice || []; + if (diceTerms.length === 0) return; + + let normalIndex = null; + let stressIndex = null; + if (normalDice > 0 && stressDice > 0) { + normalIndex = 0; + stressIndex = 1; + } else if (normalDice > 0) { + normalIndex = 0; + } else if (stressDice > 0) { + stressIndex = 0; + } + + // Normal dice → yellow Alien RPG d6 (skull on 1, blank 2-5, success on 6) + if (normalIndex !== null && diceTerms[normalIndex]) { + diceTerms[normalIndex].options = diceTerms[normalIndex].options || {}; + diceTerms[normalIndex].options.appearance = { system: "slazero2" }; + } + + // Stress dice → black Alien RPG d6 (blank 1-5, success on 6) + if (stressIndex !== null && diceTerms[stressIndex]) { + diceTerms[stressIndex].options = diceTerms[stressIndex].options || {}; + diceTerms[stressIndex].options.appearance = { system: "ze-stress-dice" }; + } + } + + /** + * Derive immediate self-only stat changes from panic effect text + * @param {string} text + * @returns {object|null} + * @private + */ + _derivePanicSelfChangesFromText(text = "") { + const changes = {}; + const sentences = String(text) + .split(".") + .map(s => s.trim()) + .filter(Boolean); + + for (const sentence of sentences) { + // Skip non-immediate or non-self effects. + if (/(allies|ally|if you refuse|per round|until end of scene|gm chooses|choose:)/i.test(sentence)) { + continue; + } + + let match = sentence.match(/\bGain\s*\+?(\d+)\s*(Stress|Resolve|Health)\b/i); + if (match) { + const value = Number(match[1]) || 0; + const stat = String(match[2] || "").toLowerCase(); + if (value > 0 && stat) changes[stat] = (changes[stat] || 0) + value; + continue; + } + + match = sentence.match(/\b(Lose|Suffer)\s+(\d+)\s*(Stress|Resolve|Health)\b/i); + if (match) { + const value = Number(match[2]) || 0; + const stat = String(match[3] || "").toLowerCase(); + if (value > 0 && stat) changes[stat] = (changes[stat] || 0) - value; + } + } + + return Object.keys(changes).length ? changes : null; + } + + /** + * Get panic effect for a given total + * @param {number} total - The panic roll total + * @returns {Object} Panic effect details + * @private + */ + _getPanicEffect(total) { + const effects = { + 7: { name: "Nervous Twitch", text: "Gain +1 Stress. All allies in Short range gain +1 Stress as your tension spikes the squad.", directive: "" }, + 8: { name: "Tremble", text: "Until end of scene: -2 dice to all AGI-based rolls (Marksmanship, Mobility, Stealth).", directive: "tremble" }, + 9: { name: "Fumble/Jam", text: "GM chooses one: drop held item; weapon jams (1 Fast action to clear); comms/camera mishandled (next related roll -2 dice).", directive: "" }, + 10: { name: "Freeze", text: "Lose your next action. You may defend normally but cannot Push again until you act.", directive: "freeze" }, + 11: { name: "Seek Cover", text: "Use next action moving to cover/safer position. If you refuse, take +1 Stress per round.", directive: "seekCover" }, + 12: { name: "Outburst on Camera", text: "You shout/scream/swear. Lose next action; position revealed (enemies +2 dice to locate).", directive: "" }, + 13: { name: "Flight Response", text: "Must retreat from threat for at least 1 round or one zone away. Cannot take Slow actions during retreat.", directive: "flight" }, + 14: { name: "Operative Breakdown", text: "Choose: BERSERK (attack nearest creature next action, friend/foe) OR CATATONIC (incapacitated D6 rounds). Allies who witness gain +1 Stress.", directive: "breakdown" }, + 15: { name: "Mind Fracture", text: "Immediately suffer a Mental Critical Injury (rolling on Mental Critical Table now).", directive: "", autoMentalCritical: true } + }; + + const effectTotal = Math.min(total, 15); + const effect = effects[effectTotal] || effects[15]; + const derivedChanges = this._derivePanicSelfChangesFromText(effect?.text || ""); + if (derivedChanges) { + return { ...effect, selfChanges: { ...(effect.selfChanges || {}), ...derivedChanges } }; + } + return effect; + } + + /** + * Roll Mental Critical Injury (D66) + * @param {Actor} actor - The actor suffering the critical + * @private + */ + async _rollMentalCritical(actor) { + // Roll D66 as single Roll so Dice So Nice animates both dice together + const d66Roll = new Roll('1d6 + 1d6', {}); + await d66Roll.evaluate(); + + // Apply alien dice skin — force slazero2 system via appearance + for (const term of (d66Roll.dice || [])) { + term.options = term.options || {}; + term.options.appearance = { system: "slazero2" }; + } + + const tens = d66Roll.dice[0].results[0].result; + const ones = d66Roll.dice[1].results[0].result; + const key = `${tens}.${ones}`; + + // Get critical from lookup + const critical = this._getMentalCritical(key); + + // Add to actor's critical injuries + const injuries = actor.system.criticalInjuries.mental || []; + injuries.push({ + key: key, + name: critical.name, + lethal: critical.lethal, + timeLimit: critical.timeLimit || "", + effects: critical.effects, + recovery: critical.recovery, + timestamp: Date.now() + }); + + await actor.update({ 'system.criticalInjuries.mental': injuries }); + + // Create chat card + let content = '
'; + content += `

Rolled: ${tens}${ones} → ${tens}.${ones}

`; + content += `

${critical.name}`; + if (critical.lethal) { + content += ' ☠️ LETHAL'; + } + content += '

'; + + if (critical.timeLimit) { + content += `

Time Limit: ${critical.timeLimit}

`; + } + + content += `

Effects: ${critical.effects}

`; + content += `

Recovery: ${critical.recovery}

`; + content += '
'; + + await ChatMessage.create({ + speaker: ChatMessage.getSpeaker({ actor }), + flavor: `💔 MENTAL CRITICAL INJURY`, + content: content, + rolls: [d66Roll] + }); + + // Auto-apply condition from this mental crit result + const MENTAL_CRIT_CONDITIONS = { + '1.1': ['sla-shaken'], '1.2': ['sla-shaken'], '1.3': ['sla-shaken'], + '1.4': ['sla-shaken'], '1.5': ['sla-panicking'], '1.6': ['sla-panicking'], + '2.1': ['sla-shaken'], '2.2': ['sla-panicking'], '2.3': ['sla-panicking'], + '2.4': ['sla-shaken'], '2.5': ['sla-panicking'], '2.6': ['sla-panicking'], + '3.1': ['sla-panicking'], '3.2': ['sla-panicking'], '3.3': ['sla-shaken'], + '3.4': ['sla-panicking'], '3.5': ['sla-panicking'], '3.6': ['sla-panicking'], + '4.1': ['sla-panicking'], '4.2': ['sla-panicking'], '4.3': ['sla-panicking'], + '4.4': ['sla-panicking'], '4.5': ['sla-panicking'], '4.6': ['sla-panicking'], + '5.1': ['sla-panicking'], '5.2': ['sla-panicking'], '5.3': ['sla-panicking'], + '5.4': ['sla-panicking'], '5.5': ['sla-panicking'], '5.6': ['sla-panicking'], + '6.1': [], '6.2': [], '6.3': [], '6.4': [], '6.5': [], '6.6': [] + }; + const mCondIds = MENTAL_CRIT_CONDITIONS[key] || []; + for (const condId of mCondIds) { + await _applyStatusCondition(actor, condId); + } + + // If instant death + if (critical.timeLimit === 'Instant') { + ui.notifications.error(`${actor.name} has died from ${critical.name}!`); + } + } + + /** + * Get mental critical injury details + * @param {string} key - The D66 key (e.g., "3.4") + * @returns {Object} Critical injury details + * @private + */ + _getMentalCritical(key) { + // Simplified lookup - in production, load from tables-data.json + const criticals = { + "1.1": { name: "Shaken", lethal: false, effects: "+1 Stress; -1 die to all rolls for 1 round.", recovery: "1 stretch" }, + "1.2": { name: "Tunnel vision", lethal: false, effects: "-2 dice to Observation; you miss side details.", recovery: "1 shift" }, + "2.1": { name: "Anxiety spiral", lethal: false, effects: "+1 Stress each shift until safe rest stretch.", recovery: "D6 shifts" }, + "3.1": { name: "Rage trigger", lethal: false, effects: "When harmed/insulted: pass Resolve check or retaliate next action.", recovery: "2D6 days" }, + "4.1": { name: "Major breakdown", lethal: false, effects: "Broken for D6 rounds; afterward -2 dice to mental rolls for 1 day.", recovery: "D6 days" }, + "5.1": { name: "Permanent phobia", lethal: false, effects: "As Phobia but only therapy/arc resolution removes it.", recovery: "Permanent until treated" }, + "6.1": { name: "Heart seizure", lethal: true, timeLimit: "1 shift", effects: "Each shift make Stamina; fail = death.", recovery: "—" }, + "6.6": { name: "Heart stops", lethal: true, timeLimit: "Instant", effects: "You die immediately.", recovery: "—" } + }; + + return criticals[key] || { name: "Shaken", lethal: false, effects: "+1 Stress; -1 die to all rolls for 1 round.", recovery: "1 stretch" }; + } + + /** + * Roll Physical Critical Injury (D66) + * @param {Actor} actor - The actor suffering the critical + * @private + */ + async _rollPhysicalCritical(actor) { + // Roll D66 as single Roll so Dice So Nice animates both dice together + const d66Roll = new Roll('1d6 + 1d6', {}); + await d66Roll.evaluate(); + + // Apply alien dice skin — force slazero2 system via appearance + for (const term of (d66Roll.dice || [])) { + term.options = term.options || {}; + term.options.appearance = { system: "slazero2" }; + } + + const tens = d66Roll.dice[0].results[0].result; + const ones = d66Roll.dice[1].results[0].result; + const key = `${tens}.${ones}`; + + // Get critical from lookup + const critical = this._getPhysicalCritical(key); + + // Add to actor's critical injuries + const injuries = actor.system.criticalInjuries.physical || []; + injuries.push({ + key: key, + name: critical.name, + lethal: critical.lethal, + timeLimit: critical.timeLimit || "", + effects: critical.effects, + healing: critical.healing, + timestamp: Date.now() + }); + + await actor.update({ 'system.criticalInjuries.physical': injuries }); + + // Create chat card + let content = '
'; + content += `

Rolled: ${tens}${ones} → ${tens}.${ones}

`; + content += `

${critical.name}`; + if (critical.lethal) { + content += ' ☠️ LETHAL'; + } + content += '

'; + + if (critical.timeLimit) { + content += `

Time Limit: ${critical.timeLimit}

`; + } + + content += `

Effects: ${critical.effects}

`; + content += `

Healing: ${critical.healing}

`; + content += '
'; + + await ChatMessage.create({ + speaker: ChatMessage.getSpeaker({ actor }), + flavor: `🩸 PHYSICAL CRITICAL INJURY`, + content: content, + rolls: [d66Roll] + }); + + // Auto-apply condition from this crit result + const PHYS_CRIT_CONDITIONS = { + '1.1': ['sla-winded'], '1.2': ['sla-winded'], '1.3': ['sla-winded'], + '1.4': ['sla-winded'], '1.5': ['sla-bleeding'], '1.6': ['sla-bleeding'], + '2.1': ['sla-winded'], '2.2': ['sla-broken-arm'], '2.3': ['sla-bleeding'], + '2.4': ['sla-broken-arm'], '2.5': ['sla-bleeding'], '2.6': ['sla-bleeding'], + '3.1': ['sla-broken-leg'], '3.2': ['sla-broken-leg'], '3.3': ['sla-concussed'], + '3.4': ['sla-broken-arm'], '3.5': ['sla-bleeding', 'sla-gut-wound'], '3.6': ['sla-gut-wound'], + '4.1': ['sla-bleeding', 'sla-suppressed'], '4.2': ['sla-broken-arm'], + '4.3': ['sla-bleeding'], '4.4': ['sla-gut-wound'], + '4.5': ['sla-bleeding'], '4.6': ['sla-bleeding'], + '5.1': ['sla-bleeding'], '5.2': ['sla-bleeding'], + '5.3': ['sla-bleeding'], '5.4': ['sla-bleeding'], + '5.5': ['sla-stunned'], '5.6': ['sla-stunned'], + '6.1': [], '6.2': [], '6.3': [], '6.4': [], '6.5': [], '6.6': [] + }; + const condIds = PHYS_CRIT_CONDITIONS[key] || []; + for (const condId of condIds) { + await _applyStatusCondition(actor, condId); + } + + // If instant death + if (critical.timeLimit === 'Instant') { + ui.notifications.error(`${actor.name} has died from ${critical.name}!`); + } + } + + /** + * Get physical critical injury details + * @param {string} key - The D66 key (e.g., "3.4") + * @returns {Object} Critical injury details + * @private + */ + _getPhysicalCritical(key) { + // Simplified lookup - in production, load from tables-data.json + const criticals = { + "1.1": { name: "Winded", lethal: false, effects: "Lose next Fast action; -1 die to physical rolls for 1 round.", healing: "1 stretch" }, + "1.2": { name: "Sprained wrist", lethal: false, effects: "-2 dice to one-handed actions with that hand.", healing: "D6 shifts" }, + "2.1": { name: "Twisted ankle", lethal: false, effects: "-2 dice to Mobility; half movement speed.", healing: "D6 shifts" }, + "2.2": { name: "Concussion", lethal: false, effects: "-2 dice to Observation & Wits rolls; cannot Push.", healing: "D6 days" }, + "3.1": { name: "Broken ribs", lethal: false, effects: "-2 dice to Stamina; Slow actions cause 1 damage.", healing: "2D6 days" }, + "3.2": { name: "Deep laceration", lethal: false, effects: "-1 die to all physical rolls; 1 damage per shift unhealed.", healing: "D6 days" }, + "4.1": { name: "Punctured lung", lethal: true, timeLimit: "1 shift", effects: "-2 dice to Stamina & Mobility; each round exert = 1 damage.", healing: "D6 days" }, + "4.2": { name: "Broken arm", lethal: false, effects: "Cannot use that arm for any action.", healing: "2D6 days" }, + "5.1": { name: "Ruptured jugular", lethal: true, timeLimit: "1 round", effects: "Broken; -1 Health per round until stabilized.", healing: "D6 days after stabilized" }, + "5.2": { name: "Internal bleeding", lethal: true, timeLimit: "1 shift", effects: "-1 Health per shift until stabilized.", healing: "2D6 days" }, + "6.1": { name: "Crushed skull", lethal: true, timeLimit: "Instant", effects: "You die immediately.", healing: "—" }, + "6.6": { name: "Catastrophic destruction", lethal: true, timeLimit: "Instant", effects: "You die immediately.", healing: "—" } + }; + + return criticals[key] || { name: "Winded", lethal: false, effects: "Lose next Fast action; -1 die to physical rolls for 1 round.", healing: "1 stretch" }; + } +} + +/** + * Initialize the system + */ +Hooks.once('init', function() { + console.log('SLA Zero Two | Initializing'); + + if (!ZERO_ENGINE_CORE_COMBAT.captured) { + ZERO_ENGINE_CORE_COMBAT.combatClass = CONFIG.Combat?.documentClass ?? null; + ZERO_ENGINE_CORE_COMBAT.combatantClass = CONFIG.Combatant?.documentClass ?? null; + ZERO_ENGINE_CORE_COMBAT.combatTrackerClass = CONFIG.ui?.combat ?? null; + ZERO_ENGINE_CORE_COMBAT.captured = true; + } + + // Register Handlebars helpers for SLA Industries templates + Handlebars.registerHelper('eq', function(a, b) { + return a === b; + }); + + Handlebars.registerHelper('gt', function(a, b) { + return a > b; + }); + + Handlebars.registerHelper('lt', function(a, b) { + return a < b; + }); + + Handlebars.registerHelper('lte', function(a, b) { + return a <= b; + }); + + Handlebars.registerHelper('contains', function(array, value) { + if (!Array.isArray(array)) return false; + return array.includes(value); + }); + + Handlebars.registerHelper('concat', function(...args) { + args.pop(); // Remove Handlebars options object + return args.join(''); + }); + + Handlebars.registerHelper('toUpperCase', function(str) { + return String(str).toUpperCase(); + }); + + Handlebars.registerHelper('capitalize', function(str) { + if (!str) return ''; + return String(str).charAt(0).toUpperCase() + String(str).slice(1); + }); + + // Mk2 template helpers + Handlebars.registerHelper('add', function(...args) { + // last arg is Handlebars options hash — drop it + const nums = args.slice(0, -1); + return nums.reduce((sum, n) => sum + (Number(n) || 0), 0); + }); + Handlebars.registerHelper('minus', function(a, b) { return (Number(a) || 0) - (Number(b) || 0); }); + Handlebars.registerHelper('times', function(n, options) { + let out = ''; + for (let i = 0; i < Number(n); i++) out += options.fn({ index: i }); + return out; + }); + Handlebars.registerHelper('or', function(...args) { + const opts = args[args.length - 1]; + const vals = args.slice(0, -1); + const result = vals.some(Boolean); + // Block helper usage: {{#or a b}}...{{/or}} + if (opts && typeof opts.fn === 'function') { + return result ? opts.fn(this) : (typeof opts.inverse === 'function' ? opts.inverse(this) : ''); + } + // Inline helper usage: {{or a b}} + return result; + }); + Handlebars.registerHelper('gte', function(a, b) { return Number(a) >= Number(b); }); + Handlebars.registerHelper('not', function(val) { return !val; }); + + console.log('SLA Zero Two | Handlebars helpers registered'); + + // Register sheet application classes + foundry.documents.collections.Actors.unregisterSheet("core", foundry.appv1.sheets.ActorSheet); + foundry.documents.collections.Actors.registerSheet("slazero2", ZeroEngineActorSheet, { + makeDefault: true, + label: "SLA Zero Two Character Sheet" + }); + + // Register item sheet + foundry.documents.collections.Items.unregisterSheet("core", foundry.appv1.sheets.ItemSheet); + foundry.documents.collections.Items.registerSheet("slazero2", ZeroEngineItemSheet, { + makeDefault: true, + label: "SLA Zero Two Item Sheet" + }); + + // Register Mk2 dark sheet as an alternative (ZeroEngineActorSheetMk2 defined later in file; + // Foundry calls this callback after the full script evaluates, so the class exists by then) + foundry.documents.collections.Actors.registerSheet("slazero2", ZeroEngineActorSheetMk2, { + types: ["character"], + makeDefault: false, + label: "SLA Zero Two — Mk2 (Dark)" + }); + + console.log('SLA Zero Two | Actor and Item sheets registered'); + + // Register SLA combat conditions as Foundry status effects (token HUD icons) + // SLA_CONDITIONS is defined later in the file but the module executes fully + // before any Hook callbacks run, so the constant exists by the time this fires. + Hooks.once("setup", () => { + for (const cond of SLA_CONDITIONS) { + const existing = CONFIG.statusEffects.find(e => e.id === cond.id); + if (!existing) { + CONFIG.statusEffects.push({ + id: cond.id, + name: cond.label, + icon: cond.icon, + label: cond.label + }); + } + } + console.log("SLA Zero Two | SLA combat conditions registered"); + }); +}); + +Hooks.once("setup", () => { + // Apply after all module init hooks so card-based module overrides are reverted. + restoreZeroEngineCoreCombatConfig(); + ensureCombatantCompatibilityMethods(); +}); + +/** + * Ready hook + */ +// ── GM SCENE CONTROLS: SLA TOOLS ───────────────────────────────────────────── +// Adds all SLA GM tool buttons to the scene controls (GM only). +// Foundry v14: controls is an object; v13: controls is an array. +Hooks.on("getSceneControlButtons", (controls) => { + if (!game.user?.isGM) return; + + const SLA_TOOLS = [ + { name: "sla-pc-status", title: "PC Status Board", icon: "fas fa-clipboard-list", fn: () => SLAGMStatusWindow.open() }, + { name: "sla-credit", title: "Credit Distribution", icon: "fas fa-credit-card", fn: () => SLACreditDistributionTool.open() }, + { name: "sla-ledger", title: "Shift Ledger", icon: "fas fa-book-open", fn: () => SLAShiftLedger.open() }, + { name: "sla-threat", title: "NPC Threat Board", icon: "fas fa-skull", fn: () => SLANPCThreatBoard.open() }, + { name: "sla-bpn", title: "BPN Tracker", icon: "fas fa-clipboard-check", fn: () => SLABPNTracker.open() }, + { name: "sla-condapp", title: "Condition Applicator", icon: "fas fa-bolt", fn: () => SLAConditionApplicator.open() }, + ]; + + // Foundry v14: controls is a plain object keyed by group name + if (controls && !Array.isArray(controls)) { + const grp = controls.tokens ?? controls.token ?? null; + if (grp) { + grp.tools ??= {}; + for (const t of SLA_TOOLS) { + grp.tools[t.name] = { + name: t.name, title: t.title, icon: t.icon, + visible: true, button: true, toggle: false, + onChange: t.fn + }; + } + } + return; + } + + // Foundry v13 fallback: controls is an array + const tokenGroup = controls.find(c => c.name === "token" || c.name === "tokens"); + if (tokenGroup) { + tokenGroup.tools ??= []; + for (const t of SLA_TOOLS) { + const entry = { + name: t.name, title: t.title, icon: t.icon, + visible: true, button: true, + onChange: t.fn, onClick: t.fn + }; + if (Array.isArray(tokenGroup.tools)) tokenGroup.tools.push(entry); + else tokenGroup.tools[t.name] = entry; + } + } +}); + +Hooks.once('ready', function() { + console.log('SLA Zero Two | Ready'); + + // Hourly PC condition report — whispered to GM (fires once per hour while Foundry is running) + if (game.user?.isGM) { + setInterval(() => _broadcastPCConditions(), 60 * 60 * 1000); + } + + // ── ONE-TIME DATA SANITIZATION ──────────────────────────────────────── + // Fix actors whose skill/flux values were corrupted by duplicate form inputs + // (stored as "X,X" string). Safe to run every boot — skips clean actors. + if (game.user?.isGM) { + (async () => { + for (const actor of game.actors ?? []) { + const updates = {}; + const skills = actor.system.skills ?? {}; + for (const [key, skill] of Object.entries(skills)) { + const raw = skill?.value; + const isCorrupt = (typeof raw === 'string' && (raw.includes(',') || raw === 'NaN' || isNaN(Number(raw)))) + || (typeof raw === 'number' && isNaN(raw)); + if (isCorrupt) { + updates[`system.skills.${key}.value`] = parseInt(raw) || 0; + console.warn(`SLA Zero Two | Sanitizing corrupt skill ${key} on actor "${actor.name}": "${raw}" → ${updates[`system.skills.${key}.value`]}`); + } + } + const rawFlux = actor.system.flux?.value; + const fluxCorrupt = (typeof rawFlux === 'string' && (rawFlux.includes(',') || rawFlux === 'NaN' || isNaN(Number(rawFlux)))) + || (typeof rawFlux === 'number' && isNaN(rawFlux)); + if (fluxCorrupt) { + updates['system.flux.value'] = parseInt(rawFlux) || 0; + console.warn(`SLA Zero Two | Sanitizing corrupt flux on actor "${actor.name}": "${rawFlux}" → ${updates['system.flux.value']}`); + } + // Fix credits corrupted by the duplicate-input "500,500" bug + const rawCredits = actor._source?.system?.details?.credits ?? actor.system?.details?.credits; + const creditsCorrupt = (typeof rawCredits === 'string' && (rawCredits.includes(',') || rawCredits === 'NaN' || isNaN(Number(rawCredits)))) + || (typeof rawCredits === 'number' && isNaN(rawCredits)); + if (creditsCorrupt) { + const fixed = typeof rawCredits === 'string' && rawCredits.includes(',') + ? parseInt(rawCredits.split(',')[0]) || 0 + : 0; + updates['system.details.credits'] = fixed; + console.warn(`SLA Zero Two | Sanitizing corrupt credits on actor "${actor.name}": "${rawCredits}" → ${fixed}`); + } + if (Object.keys(updates).length > 0) { + await actor.update(updates); + } + } + })(); + } + + // Ensure ranged weapons have ammo/magazine/ammoType defaults + (async () => { + const disableYzeCardMode = async () => { + if (!game.user?.isGM || !game.modules?.get("yze-combat")?.active) return; + const safeSet = async (key, value) => { + try { await game.settings.set("yze-combat", key, value); } catch (_err) {} + }; + await safeSet("initAutoDraw", false); + await safeSet("initMessaging", false); + await safeSet("initResetDeckOnCombatStart", false); + await safeSet("resetEachRound", false); + // Higher initiative should act first. + await safeSet("initSortOrder", -1); + }; + await disableYzeCardMode(); + + const restoreStandardInitiative = async () => { + const yzeCombatActive = game.modules?.get("yze-combat")?.active; + if (!yzeCombatActive) return; + restoreZeroEngineCoreCombatConfig(); + ensureCombatantCompatibilityMethods(); + + for (const combat of game.combats ?? []) { + const updates = []; + for (const combatant of combat.combatants) { + // Sync combatant initiative and yze-combat display to whatever is stored + // on the actor (0 if never rolled). Use value-setting not key-deletion + // to avoid deprecated "-=" Foundry v14 syntax. + const actorInit = Number(combatant.actor?.system?.combat?.initiative ?? 0) || 0; + updates.push({ + _id: combatant.id, + initiative: actorInit, + "flags.yze-combat.cardValue": actorInit, + "flags.yze-combat.cardName": "ZE INIT" + }); + } + if (updates.length > 0) { + await combat.updateEmbeddedDocuments("Combatant", updates); + } + } + }; + + await restoreStandardInitiative(); + // Force tracker to repaint so stale yze-combat values are replaced + if (ui.combat) setTimeout(() => ui.combat.render(false), 500); + + const defaultMagazine = 10; + const clampInitiativeMod = (value) => { + const mod = Number(value) || 0; + return Math.max(-5, Math.min(5, mod)); + }; + const clampDerivedMod = (value) => { + const mod = Number(value) || 0; + return Math.max(-10, Math.min(10, mod)); + }; + const updateWeapon = async (item) => { + const data = item.system || {}; + const isRanged = data.category === "ranged"; + if (!isRanged) return; + + const updates = {}; + if (!data.magazine || data.magazine <= 0) updates["system.magazine"] = defaultMagazine; + const magValue = updates["system.magazine"] ?? data.magazine ?? 0; + if (!data.ammo || data.ammo <= 0) updates["system.ammo"] = magValue; + if (!data.ammoType) updates["system.ammoType"] = "standard"; + if (!Array.isArray(data.fireModes) || data.fireModes.length === 0) updates["system.fireModes"] = ["single"]; + if (data.autoAmmoUse === undefined || data.autoAmmoUse === null) updates["system.autoAmmoUse"] = 8; + if (data.equipped === undefined || data.equipped === null) updates["system.equipped"] = false; + + if (Object.keys(updates).length > 0) { + await item.update(updates); + } + }; + const updateInitiativeMod = async (item) => { + if (!["weapon", "armor", "equipment"].includes(item.type)) return; + + const data = item.system || {}; + const current = data.initiativeMod; + const next = clampInitiativeMod(current); + if (current === undefined || current === null || Number(current) !== next) { + await item.update({ "system.initiativeMod": next }); + } + if (item.system?.equipped === undefined || item.system?.equipped === null) { + await item.update({ "system.equipped": false }); + } + }; + const updateDerivedModFields = async (item) => { + if (!["armor", "equipment", "specialty"].includes(item.type)) return; + + const data = item.system || {}; + const currentHealth = data.healthMod; + const currentResolve = data.resolveMod; + const nextHealth = clampDerivedMod(currentHealth); + const nextResolve = clampDerivedMod(currentResolve); + const updates = {}; + + if (currentHealth === undefined || currentHealth === null || Number(currentHealth) !== nextHealth) { + updates["system.healthMod"] = nextHealth; + } + if (currentResolve === undefined || currentResolve === null || Number(currentResolve) !== nextResolve) { + updates["system.resolveMod"] = nextResolve; + } + + if (Object.keys(updates).length > 0) { + await item.update(updates); + } + }; + const updateDrugModel = async (item) => { + if (!(item.type === "equipment" && item.system?.isDrug === true)) return; + + const data = item.system || {}; + const clampSmall = (value) => Math.max(-10, Math.min(10, Number(value) || 0)); + const clampQty = (value) => Math.max(0, Number(value) || 0); + const updates = {}; + + if (data.quantity === undefined || data.quantity === null || Number(data.quantity) < 0) updates["system.quantity"] = clampQty(data.quantity ?? 1); + if (data.active === undefined || data.active === null) updates["system.active"] = false; + if (data.withdrawalActive === undefined || data.withdrawalActive === null) updates["system.withdrawalActive"] = false; + if (data.activeDuration === undefined || data.activeDuration === null) updates["system.activeDuration"] = "1 shift"; + if (data.withdrawalDuration === undefined || data.withdrawalDuration === null) updates["system.withdrawalDuration"] = ""; + + const numericPaths = [ + "healthMod", "healthHeal", "resolveMod", "statPhysicalMod", "skillAllMod", "panicReduction", + "injuryPenaltyIgnore", "stressRecoveryBonus", "postUseStaminaDamage", + "wdHealthMod", "wdResolveMod", "wdStatPhysicalMod", "wdSkillAllMod", "wdPanicReduction", + "wdInjuryPenaltyIgnore", "wdStressRecoveryBonus", "wdPostUseStaminaDamage" + ]; + for (const key of numericPaths) { + const next = clampSmall(data[key]); + if (data[key] === undefined || data[key] === null || Number(data[key]) !== next) { + updates[`system.${key}`] = next; + } + } + + const skillKeys = ["force", "melee", "stamina", "marksmanship", "mobility", "stealth", "crafting", "observation", "survival", "healing", "insight", "persuasion"]; + for (const key of skillKeys) { + const current = Number(data.skillMods?.[key] ?? 0); + const next = clampSmall(current); + if (data.skillMods?.[key] === undefined || current !== next) { + updates[`system.skillMods.${key}`] = next; + } + const wdCurrent = Number(data.wdSkillMods?.[key] ?? 0); + const wdNext = clampSmall(wdCurrent); + if (data.wdSkillMods?.[key] === undefined || wdCurrent !== wdNext) { + updates[`system.wdSkillMods.${key}`] = wdNext; + } + } + + if (Object.keys(updates).length > 0) { + await item.update(updates); + } + }; + const updateArmorModel = async (item) => { + if (item.type !== "armor") return; + + const data = item.system || {}; + const updates = {}; + const legacyArmor = Number(data.armorRating || data.armor || 0); + const normalizeStatTarget = (value, statMod) => { + const raw = String(value || "").trim().toLowerCase(); + if (raw === "strength" || raw === "agility" || raw === "") return raw; + if (raw.includes("str")) return "strength"; + if (raw.includes("agi")) return "agility"; + // Legacy broad "physical" text: map to agility by default for mobility-heavy usage. + if (raw.includes("physical")) return Number(statMod || 0) === 0 ? "" : "agility"; + return ""; + }; + const normalizeSkillTarget = (value, skillMod) => { + const raw = String(value || "").trim().toLowerCase(); + if (raw === "force" || raw === "mobility" || raw === "stealth" || raw === "") return raw; + if (raw.includes("force")) return "force"; + if (raw.includes("mobility")) return "mobility"; + if (raw.includes("stealth")) return "stealth"; + return Number(skillMod || 0) === 0 ? "" : ""; + }; + + if (data.armorDice === undefined || data.armorDice === null) updates["system.armorDice"] = Math.max(0, legacyArmor); + if (data.armorAuto === undefined || data.armorAuto === null) updates["system.armorAuto"] = 0; + if (data.statMod === undefined || data.statMod === null) updates["system.statMod"] = 0; + if (data.skillMod === undefined || data.skillMod === null) updates["system.skillMod"] = 0; + if (data.statModTarget === undefined || data.statModTarget === null) updates["system.statModTarget"] = ""; + if (data.skillModTarget === undefined || data.skillModTarget === null) updates["system.skillModTarget"] = ""; + if (data.healthMod === undefined || data.healthMod === null) updates["system.healthMod"] = 0; + if (data.resolveMod === undefined || data.resolveMod === null) updates["system.resolveMod"] = 0; + if (data.equipped === undefined || data.equipped === null) updates["system.equipped"] = false; + + const nextDice = Math.max(0, Number((updates["system.armorDice"] ?? data.armorDice ?? legacyArmor)) || 0); + const nextAuto = Math.max(0, Number((updates["system.armorAuto"] ?? data.armorAuto ?? 0)) || 0); + const nextStat = Math.max(-5, Math.min(5, Number((updates["system.statMod"] ?? data.statMod ?? 0)) || 0)); + const nextSkill = Math.max(-5, Math.min(5, Number((updates["system.skillMod"] ?? data.skillMod ?? 0)) || 0)); + const nextStatTarget = normalizeStatTarget((updates["system.statModTarget"] ?? data.statModTarget), nextStat); + const nextSkillTarget = normalizeSkillTarget((updates["system.skillModTarget"] ?? data.skillModTarget), nextSkill); + + if (Number(data.armorDice ?? legacyArmor) !== nextDice) updates["system.armorDice"] = nextDice; + if (Number(data.armorAuto ?? 0) !== nextAuto) updates["system.armorAuto"] = nextAuto; + if (Number(data.statMod ?? 0) !== nextStat) updates["system.statMod"] = nextStat; + if (Number(data.skillMod ?? 0) !== nextSkill) updates["system.skillMod"] = nextSkill; + if (String(data.statModTarget ?? "") !== nextStatTarget) updates["system.statModTarget"] = nextStatTarget; + if (String(data.skillModTarget ?? "") !== nextSkillTarget) updates["system.skillModTarget"] = nextSkillTarget; + if (Number(data.armorRating ?? legacyArmor) !== nextDice) updates["system.armorRating"] = nextDice; + + if (Object.keys(updates).length > 0) { + await item.update(updates); + } + }; + + for (const item of game.items) { + await updateWeapon(item); + await updateArmorModel(item); + await updateInitiativeMod(item); + await updateDerivedModFields(item); + await updateDrugModel(item); + } + for (const actor of game.actors) { + for (const item of actor.items) { + await updateWeapon(item); + await updateArmorModel(item); + await updateInitiativeMod(item); + await updateDerivedModFields(item); + await updateDrugModel(item); + } + if (actor.type === "character") { + await recalcDerivedStats(actor, { _zeAutoCriticalInternal: true }); + } + } + + // Ensure default armor templates exist as world items + try { + if (game.user?.isGM) { + const armorResponse = await fetch("systems/slazero2/packs/armors.json"); + if (armorResponse.ok) { + const armorTemplates = await armorResponse.json(); + for (const armorData of armorTemplates) { + if (!armorData || armorData.type !== "armor" || !armorData.name) continue; + const exists = game.items.some(i => i.type === "armor" && i.name === armorData.name); + if (exists) continue; + await Item.create(armorData); + } + } + } + } catch (err) { + console.warn("SLA Zero Two | Failed to ensure default armor items", err); + } + + // Ensure default drug templates exist as world items + try { + if (game.user?.isGM) { + const drugResponse = await fetch("systems/slazero2/packs/drugs.json"); + if (drugResponse.ok) { + const drugTemplates = await drugResponse.json(); + for (const drugData of drugTemplates) { + if (!drugData || drugData.type !== "equipment" || !drugData.name) continue; + const exists = game.items.some(i => i.type === "equipment" && i.system?.isDrug === true && i.name === drugData.name); + if (exists) continue; + await Item.create(drugData); + } + } + } + } catch (err) { + console.warn("SLA Zero Two | Failed to ensure default drug items", err); + } + + // Ensure default ebb formula items exist as world items (also patches img on existing items) + try { + if (game.user?.isGM) { + const ebbResponse = await fetch("systems/slazero2/packs/ebb-formulae.json"); + if (ebbResponse.ok) { + const ebbFormulae = await ebbResponse.json(); + for (const ebbData of ebbFormulae) { + if (!ebbData || ebbData.type !== "ebb" || !ebbData.name) continue; + const existing = game.items.find(i => i.type === "ebb" && i.name === ebbData.name); + if (existing) { + // Patch img if it doesn't match the pack (handles icon migrations) + if (existing.img !== ebbData.img) { + await existing.update({ img: ebbData.img }); + } + continue; + } + await Item.create(ebbData); + } + } + } + } catch (err) { + console.warn("SLA Zero Two | Failed to ensure default ebb formula items", err); + } + + // Ensure critical failure roll tables exist + try { + const response = await fetch("systems/slazero2/packs/tables-data.json"); + if (response.ok) { + const tablesData = await response.json(); + const ensureTable = async (tableData) => { + if (!tableData || !tableData.name || !Array.isArray(tableData.results)) return; + if (game.tables.getName(tableData.name)) return; + const results = tableData.results.map((r) => ({ + type: CONST.TABLE_RESULT_TYPES.TEXT, + text: r.text, + range: r.range, + weight: 1, + drawn: false + })); + await RollTable.create({ + name: tableData.name, + formula: tableData.formula || "2d6", + results: results + }); + }; + + await ensureTable(tablesData.rangedCriticalFail); + await ensureTable(tablesData.meleeCriticalFail); + } + } catch (err) { + console.warn("SLA Zero Two | Failed to ensure critical fail tables", err); + } + })(); +}); + +Hooks.on("createCombatant", async (combatant, _data, _options, userId) => { + if (game.user?.id !== userId) return; + const actorInit = Number(combatant.actor?.system?.combat?.initiative ?? 0) || 0; + const updates = { + "flags.yze-combat.cardValue": actorInit + }; + if (Number(combatant.initiative ?? 0) !== actorInit) { + updates.initiative = actorInit; + } + await combatant.update(updates); +}); + +Hooks.on("renderCombatTracker", (app, html) => { + const jq = html instanceof jQuery ? html : $(html); + const viewedCombat = app?.viewed || game.combat; + if (!viewedCombat) return; + + jq.find(".token-initiative .roll").each((_i, el) => { + const btn = $(el); + btn.off("click.zeroEngineNoCardInit"); + btn.on("click.zeroEngineNoCardInit", ev => { + ev.preventDefault(); + ev.stopPropagation(); + ui.notifications.info("Use AGI or WIT button for initiative."); + return false; + }); + }); + + const rollForCombatant = async (combatant, useMobility) => { + const actor = combatant?.actor; + if (!actor) return; + if (!game.user?.isGM && !actor.isOwner) return; + const sheet = actor.sheet instanceof ZeroEngineActorSheet + ? actor.sheet + : new ZeroEngineActorSheet(actor); + await sheet._rollInitiative(useMobility, combatant.id, viewedCombat.id); + }; + + jq.find("li.combatant").each((_idx, li) => { + const row = $(li); + if (row.find(".ze-combatant-init-controls").length) return; + const combatantId = String(row.data("combatant-id") || ""); + if (!combatantId) return; + + const controls = $(` +
+ ${Number.isFinite(Number(viewedCombat.combatants.get(combatantId)?.initiative)) ? Number(viewedCombat.combatants.get(combatantId)?.initiative) : "-"} + + +
+ `); + + const nameNode = row.find(".token-name").first(); + if (nameNode.length) nameNode.append(controls); + else row.append(controls); + + controls.find(".ze-row-init-btn").on("click", async ev => { + ev.preventDefault(); + ev.stopPropagation(); + const mode = String($(ev.currentTarget).data("mode") || "agi"); + const combatant = viewedCombat.combatants.get(combatantId); + if (!combatant) return; + await rollForCombatant(combatant, mode === "agi"); + const refreshed = viewedCombat.combatants.get(combatantId); + const nextVal = Number(refreshed?.initiative); + controls.find(".ze-init-score").text(Number.isFinite(nextVal) ? String(nextVal) : "-"); + app.render(false); + }); + }); +}); + +// ══ SLA Zero Two — Dice So Nice: Alien RPG dice skin ══════════════════════════ +// Normal pool dice: Alien yellow d6 (skull on 1, blank 2-5, success on 6). +// Stress pool dice: Alien black d6 (blank 1-5, success on 6). +// Two separate DSN systems so the presets don't overwrite each other. +// Roll code forces the correct system via term.options.appearance — this +// completely bypasses the user's saved DSN system preference. +Hooks.once("diceSoNiceReady", (dice3d) => { + // ── Colorsets ─────────────────────────────────────────────────────────────── + dice3d.addColorset({ + name: "ze-normal", + description: "ZE Normal Dice — Yellow (Alien)", + category: "SLA Zero Two", + foreground: ["#e3e300"], + background: ["#e3e300"], + outline: "#000000", + texture: "none", + material: "plastic", + visibility: "visible" + }); + + dice3d.addColorset({ + name: "ze-stress", + description: "ZE Stress Dice — Black (Alien)", + category: "SLA Zero Two", + foreground: ["#ffffff"], + background: ["#111111"], + outline: "#333333", + texture: "none", + material: "plastic", + visibility: "visible" + }); + + // ── Two systems — one per die type so presets don't overwrite each other ──── + dice3d.addSystem({ id: "slazero2", name: "SLA Zero Two — Normal Dice" }, "preferred"); + dice3d.addSystem({ id: "ze-stress-dice", name: "SLA Zero Two — Stress Dice" }); + + // ── Normal d6: Alien yellow dice (system: slazero2) ────────────────────── + // Face 1 = skull (bane), 2-5 = blank, 6 = alien success symbol + dice3d.addDicePreset({ + type: "d6", + labels: [ + "systems/slazero2/assets/dice/alien/alien-dice-y1.png", + "systems/slazero2/assets/dice/alien/alien-dice-y0.png", + "systems/slazero2/assets/dice/alien/alien-dice-y0.png", + "systems/slazero2/assets/dice/alien/alien-dice-y0.png", + "systems/slazero2/assets/dice/alien/alien-dice-y0.png", + "systems/slazero2/assets/dice/alien/alien-dice-y6.png", + ], + colorset: "ze-normal", + system: "slazero2", + }); + + // ── Stress d6: Alien black dice (system: ze-stress-dice) ──────────────────── + // Face 1-5 = blank, 6 = success symbol + dice3d.addDicePreset({ + type: "d6", + labels: [ + "systems/slazero2/assets/dice/alien/alien-dice-b0.png", + "systems/slazero2/assets/dice/alien/alien-dice-b0.png", + "systems/slazero2/assets/dice/alien/alien-dice-b0.png", + "systems/slazero2/assets/dice/alien/alien-dice-b0.png", + "systems/slazero2/assets/dice/alien/alien-dice-b0.png", + "systems/slazero2/assets/dice/alien/alien-dice-b6.png", + ], + colorset: "ze-stress", + system: "ze-stress-dice", + }); +}); + +/** + * Handle Push button clicks in chat + * Uses new automatic Push mechanics: re-roll all non-6s, Stress + Panic only if stress bane appears + */ +Hooks.on('renderChatMessageHTML', (message, html) => { + // html is an HTMLElement in Foundry v14 — use native DOM, not jQuery + html.querySelectorAll('.yze-push-button').forEach(btn => { + btn.addEventListener('click', async (event) => { + event.preventDefault(); + + const flags = message.flags?.['slazero2']; + if (!flags || !flags.canPush || flags.pushed) { + ui.notifications.warn('This roll cannot be pushed!'); + return; + } + + // Resolve actor via token (handles unlinked tokens correctly) + let actor = null; + if (flags.tokenId && flags.sceneId) { + const scene = game.scenes?.get(flags.sceneId); + const tokenDoc = scene?.tokens?.get(flags.tokenId); + if (tokenDoc) actor = tokenDoc.actor; + } + if (!actor) actor = game.actors.get(flags.actorId); + + if (!actor || !actor.sheet) { + ui.notifications.error('Actor not found!'); + return; + } + + await actor.sheet._onPushRoll(message); + }); + }); +}); + +/** + * Helper: read derived stat value from actor data (scalar or object shape) + * @param {Actor} actor + * @param {string} key + * @returns {number} + */ +function getDerivedValue(actor, key) { + const raw = actor?.system?.derivedStats?.[key]; + const value = Number((typeof raw === "object" ? raw?.value : raw) ?? 0); + return Number.isFinite(value) ? value : 0; +} + +/** + * Helper: trigger critical injury rolls without requiring rendered sheet state + * @param {Actor} actor + * @param {"physical"|"mental"} kind + * @returns {Promise} + */ +// ── Crit dedup guard ────────────────────────────────────────────────────────── +// Tracks when a crit was last fired per actor so auto-crit and manual button +// can't both fire in quick succession for the same event. +const _critLastFired = new Map(); // actorId → { physical?: ms, mental?: ms } + +function _critCooldownActive(actorId, kind, windowMs = 8000) { + const entry = _critLastFired.get(actorId) || {}; + const last = entry[kind] || 0; + return (Date.now() - last) < windowMs; +} + +function _critRecordFired(actorId, kind) { + const entry = _critLastFired.get(actorId) || {}; + entry[kind] = Date.now(); + _critLastFired.set(actorId, entry); +} + +async function runAutoCritical(actor, kind) { + // Skip if a crit of this kind was already rolled very recently for this actor + if (_critCooldownActive(actor.id, kind)) { + console.warn(`SLA Zero Two | Auto-crit (${kind}) skipped for ${actor.name} — already fired within cooldown window.`); + return; + } + _critRecordFired(actor.id, kind); + + const sheet = actor?.sheet; + if (kind === "physical") { + if (sheet && typeof sheet._rollPhysicalCritical === "function") { + await sheet._rollPhysicalCritical(actor); + return; + } + await ZeroEngineActorSheet.prototype._rollPhysicalCritical.call({ + _getPhysicalCritical: ZeroEngineActorSheet.prototype._getPhysicalCritical + }, actor); + return; + } + + if (sheet && typeof sheet._rollMentalCritical === "function") { + await sheet._rollMentalCritical(actor); + return; + } + await ZeroEngineActorSheet.prototype._rollMentalCritical.call({ + _getMentalCritical: ZeroEngineActorSheet.prototype._getMentalCritical + }, actor); +} + +const RACE_DERIVED_MODS = { + human: { health: 0, resolve: 0 }, + ebon: { health: 0, resolve: 0 }, + brainwaster: { health: 0, resolve: 0 }, + stormer: { health: 0, resolve: 0 }, + shaktar: { health: 0, resolve: 0 }, + wraith: { health: 0, resolve: 0 }, + frother: { health: 0, resolve: 0 } +}; + +const TRAINING_DERIVED_MODS = { + scout: { health: 0, resolve: 0 }, + investigator: { health: 0, resolve: 0 }, + corporate: { health: 0, resolve: 0 }, + medic: { health: 0, resolve: 0 }, + "ebon-investigator": { health: 0, resolve: 0 }, + "brainwaster-operative": { health: 0, resolve: 0 } +}; + +function toNumber(value, fallback = 0) { + const num = Number(value); + return Number.isFinite(num) ? num : fallback; +} + +function clampNumber(value, min, max) { + return Math.max(min, Math.min(max, toNumber(value, min))); +} + +function getEffectDerivedMod(effect, stat) { + if (!effect) return 0; + const direct = toNumber(effect.getFlag("slazero2", `${stat}Mod`), 0); + const drug = toNumber(effect.getFlag("slazero2", `drug${stat === "health" ? "Health" : "Resolve"}Mod`), 0); + const legacy = toNumber(effect.getFlag("sla-yze", `${stat}Mod`), 0); + return direct + drug + legacy; +} + +function getSpecialtyDerivedMod(item, stat) { + if (!item || item.type !== "specialty") return 0; + if (item.system?.isActive === false) return 0; + + const explicit = toNumber(item.system?.[`${stat}Mod`], 0); + if (explicit !== 0) return explicit; + + // Back-compat: existing True Grit entries may not have explicit numeric fields yet. + if (stat === "health") { + const name = String(item.name || "").trim().toLowerCase(); + if (name === "true grit") return 1; + const summary = String(item.system?.effects || "").toLowerCase(); + if (summary.includes("maximum health")) return 1; + } + + return 0; +} + +function calculateHealthResolve(actor) { + const attrs = actor?.system?.attributes || {}; + const raceKey = String(actor?.system?.race || "").trim().toLowerCase(); + const trainingKey = String(actor?.system?.training || "").trim().toLowerCase(); + + const baseHealth = toNumber(attrs?.strength?.value, 0) + toNumber(attrs?.agility?.value, 0); + const baseResolve = toNumber(attrs?.wits?.value, 0) + toNumber(attrs?.empathy?.value, 0); + + const raceMods = RACE_DERIVED_MODS[raceKey] || { health: 0, resolve: 0 }; + const trainingMods = TRAINING_DERIVED_MODS[trainingKey] || { health: 0, resolve: 0 }; + + let equipmentHealth = 0; + let equipmentResolve = 0; + let specialtyHealth = 0; + let specialtyResolve = 0; + + for (const item of actor.items || []) { + if (item.type === "armor" || item.type === "equipment") { + const equipped = item.system?.equipped === true || item.system?.isEquipped === true; + if (!equipped) continue; + equipmentHealth += toNumber(item.system?.healthMod, 0); + equipmentResolve += toNumber(item.system?.resolveMod, 0); + continue; + } + + if (item.type === "specialty") { + specialtyHealth += getSpecialtyDerivedMod(item, "health"); + specialtyResolve += getSpecialtyDerivedMod(item, "resolve"); + } + + } + + let drugHealth = 0; + let drugResolve = 0; + for (const item of actor.items || []) { + if (!(item.type === "equipment" && item.system?.isDrug === true)) continue; + if (item.system?.active) { + drugHealth += toNumber(item.system?.healthMod, 0); + drugResolve += toNumber(item.system?.resolveMod, 0); + } else if (item.system?.withdrawalActive) { + drugHealth += toNumber(item.system?.wdHealthMod, 0); + drugResolve += toNumber(item.system?.wdResolveMod, 0); + } + } + + // Back-compat path: allow Active Effects to still contribute temporary modifiers. + for (const effect of actor.effects || []) { + drugHealth += getEffectDerivedMod(effect, "health"); + drugResolve += getEffectDerivedMod(effect, "resolve"); + } + + const healthMax = Math.max(0, baseHealth + raceMods.health + trainingMods.health + equipmentHealth + specialtyHealth + drugHealth); + const resolveMax = Math.max(0, baseResolve + raceMods.resolve + trainingMods.resolve + equipmentResolve + specialtyResolve + drugResolve); + + return { + healthMax, + resolveMax, + breakdown: { + health: { + base: baseHealth, + race: raceMods.health, + training: trainingMods.health, + equipment: equipmentHealth, + specialty: specialtyHealth, + drug: drugHealth + }, + resolve: { + base: baseResolve, + race: raceMods.resolve, + training: trainingMods.resolve, + equipment: equipmentResolve, + specialty: specialtyResolve, + drug: drugResolve + } + } + }; +} + +function buildDerivedRecalcUpdate(actor) { + if (!actor || actor.type !== "character") return null; + + const calc = calculateHealthResolve(actor); + const currentHealth = getDerivedValue(actor, "health"); + const currentResolve = getDerivedValue(actor, "resolve"); + const currentHealthMax = toNumber(actor?.system?.derivedStats?.health?.max, currentHealth); + const currentResolveMax = toNumber(actor?.system?.derivedStats?.resolve?.max, currentResolve); + + const nextHealthMax = Math.max(0, calc.healthMax); + const nextResolveMax = Math.max(0, calc.resolveMax); + const nextHealth = clampNumber(currentHealth, 0, nextHealthMax); + const nextResolve = clampNumber(currentResolve, 0, nextResolveMax); + + const updates = {}; + if (currentHealthMax !== nextHealthMax) updates["system.derivedStats.health.max"] = nextHealthMax; + if (currentResolveMax !== nextResolveMax) updates["system.derivedStats.resolve.max"] = nextResolveMax; + if (currentHealth !== nextHealth) updates["system.derivedStats.health.value"] = nextHealth; + if (currentResolve !== nextResolve) updates["system.derivedStats.resolve.value"] = nextResolve; + + // Clear stale broken flag — if current HP is above 0, broken is no longer valid. + // This prevents the legacy -2 dice penalty persisting on healed characters. + if (actor.system?.derivedStats?.broken === true && nextHealth > 0) { + updates["system.derivedStats.broken"] = false; + } + + // NOTE: breakdown is NOT in template.json — do not persist it or Foundry v14 + // schema validation will reset the entire derivedStats object (nuking stress, etc.) + // Compute breakdown in getData() for display purposes only. + + return Object.keys(updates).length > 0 ? updates : null; +} + +async function recalcDerivedStats(actor, updateOptions = {}) { + const updates = buildDerivedRecalcUpdate(actor); + if (!updates) return; + await actor.update(updates, { _zeDerivedRecalcInternal: true, ...updateOptions }); +} + +function shouldRecalcForActorUpdate(change) { + if (!change || !change.system) return false; + return [ + "system.attributes.strength.value", + "system.attributes.agility.value", + "system.attributes.wits.value", + "system.attributes.empathy.value", + "system.race", + "system.training" + ].some(path => foundry.utils.hasProperty(change, path)); +} + +Hooks.on("preUpdateActor", (actor, change, options) => { + if (options?._zeDerivedRecalcInternal) return; + options._zePrevHealth = getDerivedValue(actor, "health"); + options._zePrevResolve = getDerivedValue(actor, "resolve"); +}); + +Hooks.on("createActor", async (actor, options) => { + if (actor?.type !== "character") return; + if (options?._zeDerivedRecalcInternal) return; + await recalcDerivedStats(actor); +}); + +Hooks.on("updateActor", async (actor, change, options) => { + if (actor?.type !== "character") return; + if (options?._zeDerivedRecalcInternal) return; + + // Also clear stale broken flag whenever HP is updated to > 0 + const hpChanged = foundry.utils.hasProperty(change, "system.derivedStats.health.value") || + foundry.utils.hasProperty(change, "system.derivedStats.health"); + if (hpChanged && actor.system?.derivedStats?.broken === true) { + const currentHp = Number( + typeof actor.system.derivedStats.health === "object" + ? actor.system.derivedStats.health?.value + : actor.system.derivedStats.health + ) || 0; + if (currentHp > 0) { + await actor.update({ "system.derivedStats.broken": false }, { _zeDerivedRecalcInternal: true }); + } + } + + if (!shouldRecalcForActorUpdate(change)) return; + await recalcDerivedStats(actor); + + // When race changes, auto-add any missing racial specialties for the new race + if (foundry.utils.hasProperty(change, "system.race") && game.user?.isGM) { + try { await _ensureRacialAbilities(actor); } catch(_) {} + } +}); + +Hooks.on("createItem", async (item, options) => { + const actor = item?.actor; + if (actor?.type !== "character") return; + if (options?._zeDerivedRecalcInternal) return; + if (!["armor", "equipment", "specialty"].includes(item.type)) return; + await recalcDerivedStats(actor); +}); + +Hooks.on("updateItem", async (item, change, options) => { + const actor = item?.actor; + if (actor?.type !== "character") return; + if (options?._zeDerivedRecalcInternal) return; + if (!["armor", "equipment", "specialty"].includes(item.type)) return; + await recalcDerivedStats(actor); +}); + +Hooks.on("deleteItem", async (item, options) => { + const actor = item?.actor; + if (actor?.type !== "character") return; + if (options?._zeDerivedRecalcInternal) return; + if (!["armor", "equipment", "specialty"].includes(item.type)) return; + await recalcDerivedStats(actor); +}); + +Hooks.on("createActiveEffect", async (effect, options) => { + const actor = effect?.parent; + if (actor?.documentName !== "Actor" || actor.type !== "character") return; + if (options?._zeDerivedRecalcInternal) return; + await recalcDerivedStats(actor); +}); + +Hooks.on("updateActiveEffect", async (effect, change, options) => { + const actor = effect?.parent; + if (actor?.documentName !== "Actor" || actor.type !== "character") return; + if (options?._zeDerivedRecalcInternal) return; + await recalcDerivedStats(actor); +}); + +Hooks.on("deleteActiveEffect", async (effect, options) => { + const actor = effect?.parent; + if (actor?.documentName !== "Actor" || actor.type !== "character") return; + if (options?._zeDerivedRecalcInternal) return; + await recalcDerivedStats(actor); +}); + +Hooks.on("updateActor", async (actor, change, options) => { + if (actor?.type !== "character") return; + if (options?._zeDerivedRecalcInternal) return; + if (options?._zeAutoCriticalInternal) return; + + const prevHealth = Number(options?._zePrevHealth); + const prevResolve = Number(options?._zePrevResolve); + if (!Number.isFinite(prevHealth) && !Number.isFinite(prevResolve)) return; + + const nextHealth = getDerivedValue(actor, "health"); + const nextResolve = getDerivedValue(actor, "resolve"); + + const healthBroke = Number.isFinite(prevHealth) && prevHealth > 0 && nextHealth <= 0; + const resolveBroke = Number.isFinite(prevResolve) && prevResolve > 0 && nextResolve <= 0; + if (!healthBroke && !resolveBroke) return; + + const updateOptions = { _zeAutoCriticalInternal: true }; + if (actor.system?.derivedStats?.broken !== true) { + await actor.update({ "system.derivedStats.broken": true }, updateOptions); + } + + if (healthBroke) { + ui.notifications.warn(`${actor.name} is BROKEN and suffers a Physical Critical Injury!`); + await runAutoCritical(actor, "physical"); + } + if (resolveBroke) { + ui.notifications.warn(`${actor.name} is BROKEN and suffers a Mental Critical Injury!`); + await runAutoCritical(actor, "mental"); + } +}); + +// Old showPushDialog function removed - now using automatic Push mechanics + + +// ══════════════════════════════════════════════════════════════════════════════ +// SLA INDUSTRIES — CHARACTER GENERATORS (appended to slazero2.mjs) +// PC Creator (multi-step wizard, 7 races, 8 training packages) +// NPC Creator (quick + full modes, 10 archetypes) +// ══════════════════════════════════════════════════════════════════════════════ + +// ─── RACE DATA ─────────────────────────────────────────────────────────────── +const SLA_RACES = { + human: { + label: 'Human', icon: '👤', + flavor: 'The backbone of SLA Industries. Born, raised, and usually died on Mort. Adaptable, expendable, and everywhere.', + baseAttrs: { strength: 2, agility: 2, wits: 2, empathy: 2 }, + attrPoints: 6, + attrCaps: { strength: 5, agility: 5, wits: 5, empathy: 5 }, + skillPoints: 10, skillCap: 3, + isEbbUser: false, healthMod: 0, resolveMod: 0 + }, + ebon: { + label: 'Ebon', icon: '🔮', + flavor: 'Born from the Ebb itself. Their minds reach where others cannot. SLA prizes them — and fears what they might become.', + baseAttrs: { strength: 2, agility: 2, wits: 3, empathy: 3 }, + attrPoints: 4, + attrCaps: { strength: 4, agility: 4, wits: 5, empathy: 5 }, + skillPoints: 10, skillCap: 3, + isEbbUser: true, healthMod: 0, resolveMod: 1 + }, + brainwaster: { + label: 'Brain Waster', icon: '🧠', + flavor: 'Ebon mutation gone raw. The Ebb tears through them and takes pieces every time. Brilliantly, terrifyingly unstable.', + baseAttrs: { strength: 2, agility: 2, wits: 4, empathy: 3 }, + attrPoints: 3, + attrCaps: { strength: 3, agility: 4, wits: 5, empathy: 5 }, + skillPoints: 10, skillCap: 3, + isEbbUser: true, healthMod: 0, resolveMod: 0 + }, + stormer: { + label: 'Stormer (313-S Malice)', icon: '⚡', + flavor: 'Vatgrown for war. A Stormer does not stop until the objective is complete — or it is dead. Usually the objective.', + baseAttrs: { strength: 4, agility: 3, wits: 2, empathy: 2 }, + attrPoints: 3, + attrCaps: { strength: 5, agility: 5, wits: 3, empathy: 3 }, + skillPoints: 8, skillCap: 3, + isEbbUser: false, healthMod: 2, resolveMod: 0 + }, + shaktar: { + label: 'Shaktar', icon: '🦂', + flavor: 'Alien warrior caste. Honour is their law; violence is their art. Each kill is recorded. Each debt is paid.', + baseAttrs: { strength: 3, agility: 3, wits: 2, empathy: 2 }, + attrPoints: 4, + attrCaps: { strength: 5, agility: 5, wits: 4, empathy: 4 }, + skillPoints: 9, skillCap: 3, + isEbbUser: false, healthMod: 1, resolveMod: 0 + }, + wraithraider: { + label: 'Wraith Raider', icon: '👁️', + flavor: 'They move through walls of shadow and kill without a sound. You never see them first. If you see them at all.', + baseAttrs: { strength: 2, agility: 4, wits: 2, empathy: 2 }, + attrPoints: 4, + attrCaps: { strength: 4, agility: 5, wits: 5, empathy: 4 }, + skillPoints: 10, skillCap: 3, + isEbbUser: false, healthMod: 0, resolveMod: 1 + }, + frother: { + label: 'Frother', icon: '💊', + flavor: 'The battle chemicals make them something else entirely. Pain is irrelevant. Fear is gone. Only the job remains.', + baseAttrs: { strength: 4, agility: 2, wits: 2, empathy: 2 }, + attrPoints: 4, + attrCaps: { strength: 5, agility: 4, wits: 4, empathy: 3 }, + skillPoints: 8, skillCap: 3, + isEbbUser: false, healthMod: 1, resolveMod: -1 + }, + vevaphon: { + label: 'Stormer Vevaphon', + icon: '🔀', + flavor: "Sleight Industries' cancelled polymorphic bioweapon line. Shape-shifting combat chassis with three selectable morph forms. Every shift costs Instability — and enough shifts will shatter whoever they were.", + baseAttrs: { strength: 3, agility: 3, wits: 2, empathy: 2 }, + attrPoints: 5, + attrCaps: { strength: 5, agility: 5, wits: 4, empathy: 3 }, + skillPoints: 8, skillCap: 3, + isEbbUser: false, healthMod: 2, resolveMod: -1 + } +}; + +// ─── TRAINING PACKAGES ─────────────────────────────────────────────────────── +const SLA_TRAINING = { + soldier: { + label: 'Soldier', icon: '🎖️', + flavor: 'Front-line combat. Point, shoot, advance.', + skills: { melee: 2, marksmanship: 2, stamina: 1 }, + specialty: 'Hard Hitter', + armor: 'CAF Armor', weapons: ['pistol', 'rifle'] + }, + scout: { + label: 'Scout', icon: '🎯', + flavor: 'Reconnaissance and rapid infiltration.', + skills: { stealth: 2, mobility: 2, observation: 1 }, + specialty: 'Fast Reflexes', + armor: 'Scout Armor', weapons: ['pistol', 'smg'] + }, + medic: { + label: 'Combat Medic', icon: '🩺', + flavor: 'Keeping the squad alive under fire.', + skills: { healing: 2, insight: 1, observation: 2 }, + specialty: 'Field Surgeon', + armor: 'Downtown Jacket', weapons: ['pistol'], gear: ['Medical Kit'] + }, + tech: { + label: 'Technical Operative', icon: '🔧', + flavor: 'Field engineering, hacking, and repairs.', + skills: { crafting: 3, observation: 1, survival: 1 }, + specialty: 'Inquisitive', + armor: 'Downtown Jacket', weapons: ['pistol'], gear: ['Tool Kit'] + }, + negotiator: { + label: 'Negotiator', icon: '🗣️', + flavor: 'Information, leverage, and social control.', + skills: { persuasion: 2, insight: 2, observation: 1 }, + specialty: 'Gut Feeling', + armor: 'Downtown Jacket', weapons: ['pistol'], gear: ['Encrypted Comms'] + }, + hunter: { + label: 'Hunter', icon: '🏹', + flavor: 'Track, observe, eliminate. Quietly.', + skills: { marksmanship: 2, stealth: 2, observation: 1 }, + specialty: 'Sniper', + armor: 'Scout Armor', weapons: ['rifle', 'pistol'] + }, + brawler: { + label: 'Brawler', icon: '👊', + flavor: 'Close quarters. No hesitation. Often Frother or Stormer.', + skills: { force: 2, melee: 2, stamina: 1 }, + specialty: 'True Grit', + armor: 'CAF Armor', weapons: ['heavy_melee', 'pistol'] + }, + ebb_user: { + label: 'Ebb Channeller', icon: '✨', + flavor: 'The Ebb flows through and burns bright. Ebon and Brain Waster only.', + skills: { ebb: 3, insight: 1, persuasion: 1 }, + specialty: 'Ebb Sensitive', + armor: 'Downtown Jacket', weapons: ['pistol'], + // Race-differentiated starting formulae (selected at creation time) + ebbEbon: [ + 'Empathic Pulse', // awareness — sense emotions + 'Danger Sense', // awareness — can't be surprised + 'Mind-Link', // communicate — silent mental comms + 'Calm', // heal — reduce stress + 'Ebb Shield', // protect — defensive reaction + 'Ebb Sight', // senses — detect Ebb users + 'Surge', // enhance — attribute boost + ], + ebbBrainwaster: [ + 'Ebb Bolt', // blast — direct damage + 'Heat Touch', // thermal — melee Ebb damage + 'Surge', // enhance — combat boost + 'Push', // telekinesis — hurl targets + 'Empathic Pulse', // awareness — sense surroundings + 'Ebb Shield', // protect — defensive + 'Iron Flesh', // enhance — automatic armour + ], + // Legacy fallback if race not detected + ebb: ['Empathic Pulse', 'Ebb Bolt', 'Ebb Shield', 'Calm', 'Surge', 'Ebb Sight'], + ebbOnly: true + } +}; + +// ─── NPC ARCHETYPES ────────────────────────────────────────────────────────── +const SLA_NPC_TYPES = { + civilian: { label: 'Civilian', threat: 1, attrs: {strength:2,agility:2,wits:2,empathy:2}, skills: {observation:1}, hp: 4, armor: 0, weapons: [] }, + gang_grunt: { label: 'Gang Grunt', threat: 1, attrs: {strength:3,agility:2,wits:2,empathy:2}, skills: {melee:2,force:1}, hp: 5, armor: 1, weapons: ['pistol','knife'] }, + shiver: { label: 'Shiver (Patrol)', threat: 2, attrs: {strength:3,agility:3,wits:2,empathy:2}, skills: {marksmanship:2,melee:1,observation:1}, hp: 6, armor: 2, weapons: ['pistol','baton'] }, + operative_green: { label: 'SLA Operative (Green)', threat: 2, attrs: {strength:2,agility:3,wits:2,empathy:2}, skills: {marksmanship:2,mobility:1,observation:1}, hp: 5, armor: 2, weapons: ['pistol','knife'] }, + carrien: { label: 'Carrien', threat: 2, attrs: {strength:3,agility:4,wits:1,empathy:1}, skills: {melee:3,mobility:2}, hp: 7, armor: 1, weapons: ['claws'] }, + operative_veteran: { label: 'SLA Operative (Veteran)', threat: 3, attrs: {strength:3,agility:3,wits:3,empathy:2}, skills: {marksmanship:3,melee:2,stealth:2,observation:2}, hp: 8, armor: 3, weapons: ['rifle','pistol','knife'] }, + ebb_operative: { label: 'Ebb Operative (Ebon)', threat: 3, attrs: {strength:2,agility:2,wits:4,empathy:4}, skills: {ebb:3,insight:3,observation:2}, hp: 6, armor: 1, weapons: ['pistol'], flux: 4, isEbb: true }, + dept_agent: { label: 'Dept. of Propaganda Agent', threat:3, attrs: {strength:2,agility:3,wits:4,empathy:3}, skills: {persuasion:3,insight:3,stealth:2}, hp: 6, armor: 2, weapons: ['pistol'] }, + dark_finder: { label: 'Dark Finder', threat: 4, attrs: {strength:4,agility:3,wits:3,empathy:2}, skills: {marksmanship:4,melee:3,stealth:3,observation:3}, hp: 10, armor: 4, weapons: ['rifle','pistol','blade'] }, + manchine: { label: 'Manchine', threat: 5, attrs: {strength:5,agility:4,wits:3,empathy:1}, skills: {melee:4,marksmanship:3,force:4,stamina:4}, hp: 14, armor: 5, weapons: ['heavy_weapon','claws'] } +}; + +// ─── NAME TABLES ───────────────────────────────────────────────────────────── +const SLA_NAMES = { + human: { + first: ['Mace','Nadia','Cole','Petra','Sloane','Vex','Mira','Dax','Sable','Reece','Lyra','Jet','Kade','Dani','Rex','Aura','Finn','Zara','Grim','Nova'], + last: ['Falke','Dredd','Voss','Caine','Rourke','Mace','Cross','Kane','Stern','Vane','Frost','Ryker','Blane','Dusk','Shard','Hex','Vayne','Crowe','Ash','Lorne'] + }, + ebon: { + first: ['Aelith','Cassia','Vehn','Sorai','Talen','Myren','Eshara','Quelith','Davan','Silae'], + last: ['Vel Auris','Sol Keth','Mir Daan','Thas Elen','Quor Veth','Sil Arren','Tel Mora','Van Eres'] + }, + brainwaster: { + first: ['Flux','Scorch','Null','Vex','Wraith','Bleed','Crux','Static','Glitch','Spike'], + last: ['Mindless','the Hollow','No-Name','Unbound','Fracture','Sparks','the Lost','Deadwire'] + }, + stormer: { + first: ['Slab','Iron','Breach','Razr','Blunt','Crush','Vance','Gouge','Wreck','Spike'], + last: ['313','7-Niner','Alpha-4','Echo-3','Prime','Null-Six','Striker','Unit-9','Kilo','Zero'] + }, + shaktar: { + first: ["Kah'tak","Drex'ahl","Va'shari","Mul'keth","Kar'vaan","Esh'tara","Zek'hal","Quin'dar"], + last: ["ek Valkor","ek Shath","ek Duran","ek Morvath","ek Shal","ek Tyran"] + }, + wraithraider: { + first: ['Shade','Whisper','Veil','Null','Shroud','Dusk','Mist','Ash','Fade','Echo'], + last: ['of the Void','of the Dark','the Unseen','of the Pale','the Silent','of Shadows'] + }, + frother: { + first: ['Bloody','Mad','Frenzy','Bersk','Rage','Bonehead','Wrecking','Rampant','Feral','Savage'], + last: ['McRage','O\'Kill','McGore','Killmore','Deathmarch','McCrash','Bloodstock','Warface'] + }, + generic: { + first: ['Operator','Agent','Unit','Ghost','Vector','Cipher','Reaper','Echo','Phantom','Sigma'], + last: ['Null','Seven','Prime','Alpha','Zero','Delta','Omega','Kilo','Bravo','Echo'] + } +}; + +function _slaRandomName(raceKey) { + const table = SLA_NAMES[raceKey] || SLA_NAMES.generic; + const first = table.first[Math.floor(Math.random() * table.first.length)]; + const last = table.last[Math.floor(Math.random() * table.last.length)]; + return `${first} ${last}`; +} + +// ─── GEAR BUILDERS ─────────────────────────────────────────────────────────── +// ── Inline Ebb formula data ─────────────────────────────────────────────────── +// Used when the compendium pack is unavailable (JSON packs not compiled to LDB). +// Keyed by display name — must match names used in training.ebbEbon / ebbBrainwaster. +const EBB_FORMULA_INLINE = { + // ── AWARENESS ─────────────────────────────────────────────────────────────── + 'Empathic Pulse': { discipline:'awareness', fluxCost:1, successes:1, range:'Short', duration:'Instant', effect:'Sense the dominant emotion of every living being within Short range. On 2+ successes, identify specific individuals.', img:'systems/slazero2/assets/icons/ebb/ebb-awareness.svg' }, + 'Danger Sense': { discipline:'awareness', fluxCost:2, successes:1, range:'Self', duration:'1 shift', effect:'Cannot be surprised for 1 shift. Gain +2 dice to Initiative rolls.', img:'systems/slazero2/assets/icons/ebb/ebb-awareness.svg' }, + 'Precognition': { discipline:'awareness', fluxCost:2, successes:2, range:'Self', duration:'Instant', effect:'Ask the GM one yes/no question about what will happen in the next stretch.', img:'systems/slazero2/assets/icons/ebb/ebb-awareness.svg' }, + // ── BLAST ──────────────────────────────────────────────────────────────────── + 'Ebb Bolt': { discipline:'blast', fluxCost:1, successes:1, range:'Short', duration:'Instant', effect:'Strike one target for damage equal to successes. Armour applies. Counts as ranged attack.', img:'systems/slazero2/assets/icons/ebb/ebb-blast.svg' }, + 'Hellfire': { discipline:'blast', fluxCost:2, successes:2, range:'Medium', duration:'Instant', effect:'Blast one target for successes + 2 damage. On 3+ successes, target is knocked down.', img:'systems/slazero2/assets/icons/ebb/ebb-blast.svg' }, + 'Ebb Storm': { discipline:'blast', fluxCost:3, successes:2, range:'Short', duration:'Instant', effect:'Ebb detonates at Short range. All targets take successes damage. Armour applies.', img:'systems/slazero2/assets/icons/ebb/ebb-blast.svg' }, + // ── COMMUNICATE ────────────────────────────────────────────────────────────── + 'Mind-Link': { discipline:'communicate', fluxCost:1, successes:1, range:'Short', duration:'1 scene', effect:'Establish silent two-way mental communication with one willing target.', img:'systems/slazero2/assets/icons/ebb/ebb-communicate.svg' }, + 'Broadcast': { discipline:'communicate', fluxCost:2, successes:1, range:'Long', duration:'Instant', effect:'Project one simple message, image, or emotion to all living beings within Long range.', img:'systems/slazero2/assets/icons/ebb/ebb-communicate.svg' }, + 'Compel': { discipline:'communicate', fluxCost:2, successes:3, range:'Short', duration:'1 round', effect:'Force one target to perform a simple action (flee, drop weapon, kneel). Target resists with Wits.', img:'systems/slazero2/assets/icons/ebb/ebb-communicate.svg' }, + // ── ENHANCE ────────────────────────────────────────────────────────────────── + 'Surge': { discipline:'enhance', fluxCost:1, successes:1, range:'Self', duration:'1 stretch', effect:'Add +2 dice to all rolls using one chosen attribute for 1 stretch.', img:'systems/slazero2/assets/icons/ebb/ebb-enhance.svg' }, + 'Iron Flesh': { discipline:'enhance', fluxCost:2, successes:2, range:'Self', duration:'1 stretch', effect:'Gain 2 automatic armour (stacks with worn armour) for 1 stretch.', img:'systems/slazero2/assets/icons/ebb/ebb-enhance.svg' }, + 'Heightened State':{ discipline:'enhance', fluxCost:3, successes:2, range:'Self', duration:'1 stretch', effect:'+1 die to all rolls, reduce all incoming damage by 1 for 1 stretch.', img:'systems/slazero2/assets/icons/ebb/ebb-enhance.svg' }, + // ── HEAL ───────────────────────────────────────────────────────────────────── + 'Mend': { discipline:'heal', fluxCost:1, successes:1, range:'Engaged', duration:'Instant', effect:'Restore Health equal to successes (max 2) to self or a touched target.', img:'systems/slazero2/assets/icons/ebb/ebb-heal.svg' }, + 'Restore': { discipline:'heal', fluxCost:2, successes:2, range:'Engaged', duration:'Instant', effect:'Restore Health equal to successes + 1. On 3+ successes, also clear one critical injury.', img:'systems/slazero2/assets/icons/ebb/ebb-heal.svg' }, + 'Calm': { discipline:'heal', fluxCost:1, successes:1, range:'Short', duration:'Instant', effect:"Reduce one target's Stress by 1. If they are Broken from Stress, remove the Broken state.", img:'systems/slazero2/assets/icons/ebb/ebb-heal.svg' }, + // ── PROTECT ────────────────────────────────────────────────────────────────── + 'Ebb Shield': { discipline:'protect', fluxCost:1, successes:1, range:'Self', duration:'1 round', effect:'Roll 2d6 against the next incoming attack this round; each 6 reduces damage by 1.', img:'systems/slazero2/assets/icons/ebb/ebb-protect.svg' }, + 'Ward': { discipline:'protect', fluxCost:2, successes:2, range:'Short', duration:'1 stretch', effect:'One target within Short range gains 1 automatic armour for 1 stretch.', img:'systems/slazero2/assets/icons/ebb/ebb-protect.svg' }, + 'Phase Shell': { discipline:'protect', fluxCost:3, successes:3, range:'Self', duration:'1 round', effect:'All physical damage is reduced by 3 this round. Ebb attacks bypass this.', img:'systems/slazero2/assets/icons/ebb/ebb-protect.svg' }, + // ── REALITY FOLD ───────────────────────────────────────────────────────────── + 'Blink': { discipline:'realityFold', fluxCost:2, successes:2, range:'Short', duration:'Instant', effect:'Instantly teleport to any visible point within Short range.', img:'systems/slazero2/assets/icons/ebb/ebb-realityfold.svg' }, + 'Fold Space': { discipline:'realityFold', fluxCost:3, successes:3, range:'Long', duration:'1 round', effect:'Two chosen points within range become adjacent for 1 round.', img:'systems/slazero2/assets/icons/ebb/ebb-realityfold.svg' }, + 'Phase Through': { discipline:'realityFold', fluxCost:3, successes:2, range:'Engaged', duration:'Instant', effect:'Pass through one non-living solid object (wall, door, hull).', img:'systems/slazero2/assets/icons/ebb/ebb-realityfold.svg' }, + // ── SENSES ─────────────────────────────────────────────────────────────────── + 'Ebb Sight': { discipline:'senses', fluxCost:1, successes:1, range:'Long', duration:'1 stretch', effect:'Perceive Ebb energy, active formulae, and Ebb users within Long range.', img:'systems/slazero2/assets/icons/ebb/ebb-senses.svg' }, + "Mind's Eye": { discipline:'senses', fluxCost:1, successes:1, range:'Self', duration:'1 stretch', effect:'+3 dice to all Observation rolls. Cannot be ambushed.', img:'systems/slazero2/assets/icons/ebb/ebb-senses.svg' }, + 'Remote View': { discipline:'senses', fluxCost:2, successes:2, range:'Long', duration:'1 stretch', effect:'Project senses to any location within Long range that you have visited before.', img:'systems/slazero2/assets/icons/ebb/ebb-senses.svg' }, + // ── TELEKINESIS ────────────────────────────────────────────────────────────── + 'Push': { discipline:'telekinesis', fluxCost:1, successes:1, range:'Short', duration:'Instant', effect:'Hurl one target or object with mental force, moving them up to 3m per success. Counts as ranged attack.', img:'systems/slazero2/assets/icons/ebb/ebb-telekinesis.svg' }, + 'Grip': { discipline:'telekinesis', fluxCost:2, successes:2, range:'Short', duration:'1 round', effect:'Hold one target immobile for 1 round. They can still act but cannot move.', img:'systems/slazero2/assets/icons/ebb/ebb-telekinesis.svg' }, + 'Levitate': { discipline:'telekinesis', fluxCost:2, successes:1, range:'Self', duration:'1 stretch', effect:'Float at any height, moving in any direction at normal speed.', img:'systems/slazero2/assets/icons/ebb/ebb-telekinesis.svg' }, + // ── THERMAL ────────────────────────────────────────────────────────────────── + 'Heat Touch': { discipline:'thermal', fluxCost:1, successes:1, range:'Engaged', duration:'Instant', effect:'Deal damage equal to successes + 1 to a touched target. On 3+ successes, target catches fire.', img:'systems/slazero2/assets/icons/ebb/ebb-thermal.svg' }, + 'Flame Burst': { discipline:'thermal', fluxCost:2, successes:1, range:'Short', duration:'Instant', effect:'Deal successes + 2 damage to one target. On 3+ successes, target catches fire.', img:'systems/slazero2/assets/icons/ebb/ebb-thermal.svg' }, + 'Freeze': { discipline:'thermal', fluxCost:2, successes:2, range:'Short', duration:'1 round', effect:'Target takes 1 damage and suffers -2 dice to all Agility rolls for 1 round.', img:'systems/slazero2/assets/icons/ebb/ebb-thermal.svg' }, +}; + +/** Build an embedded Ebb item document from the inline table */ +function _slaEbbFormulaData(name) { + const f = EBB_FORMULA_INLINE[name]; + if (!f) return null; + return { + name, + type: 'ebb', + img: f.img || 'systems/slazero2/assets/icons/ebb/ebb-blast.svg', + system: { + description: f.effect || '', + discipline: f.discipline, + fluxCost: f.fluxCost, + successes: f.successes, + effect: f.effect || '', + catastrophe: '', + duration: f.duration || '', + range: f.range || 'Short', + } + }; +} + +function _slaWeaponData(type) { + const weapons = { + pistol: { name: 'Blitzer 10mm Pistol', weaponType:'Pistol', category:'ranged', damage:2, range:'Short', rof:2, ap:0, magazine:15, ammo:15, fireModes:['single'], cost:300, + img: 'icons/weapons/guns/gun-pistol-flintlock-metal.webp' }, + rifle: { name: 'M300 Assault Rifle', weaponType:'Rifle', category:'ranged', damage:3, range:'Long', rof:3, ap:1, magazine:30, ammo:30, fireModes:['single','burst','auto'], cost:600, + img: 'icons/weapons/guns/rifle-bayonet.webp' }, + smg: { name: 'Blitzer SMG', weaponType:'SMG', category:'ranged', damage:2, range:'Short', rof:3, ap:0, magazine:30, ammo:30, fireModes:['single','auto'], cost:450, + img: 'icons/weapons/guns/gun-pistol-flintlock-metal.webp' }, + heavy_melee: { name: 'Power Blade', weaponType:'Blade', category:'melee', damage:3, range:'Engaged', rof:1, ap:2, magazine:0, ammo:0, fireModes:[], cost:400, + img: 'icons/weapons/swords/greatsword.webp' }, + knife: { name: 'Combat Knife', weaponType:'Knife', category:'melee', damage:2, range:'Engaged', rof:2, ap:0, magazine:0, ammo:0, fireModes:[], cost:80, + img: 'icons/weapons/daggers/dagger-straight-sharp.webp' }, + baton: { name: 'Shiver Baton', weaponType:'Baton', category:'melee', damage:2, range:'Engaged', rof:2, ap:0, magazine:0, ammo:0, fireModes:[], cost:50, + img: 'icons/weapons/clubs/club-simple-black.webp' }, + claws: { name: 'Claws', weaponType:'Natural', category:'melee', damage:3, range:'Engaged', rof:2, ap:0, magazine:0, ammo:0, fireModes:[], cost:0, + img: 'icons/skills/melee/unarmed-punch-fist.webp' }, + blade: { name: 'Mono-Edge Blade', weaponType:'Blade', category:'melee', damage:3, range:'Engaged', rof:2, ap:2, magazine:0, ammo:0, fireModes:[], cost:350, + img: 'icons/weapons/swords/sword-katana-gleaming.webp' }, + heavy_weapon: { name: 'Heavy Autocannon', weaponType:'HW', category:'ranged', damage:5, range:'Long', rof:2, ap:3, magazine:20, ammo:20, fireModes:['burst','auto'], cost:2500, + img: 'icons/weapons/guns/gun-blunderbuss.webp' }, + }; + const base = weapons[type] || weapons.pistol; + const { img, ...sys } = base; + return { name: base.name, type: 'weapon', img: img || 'icons/svg/item.svg', + system: { ...sys, description: '', equipped: true, gearBonus:0, initiativeMod:0, ammoType:'standard', autoAmmoUse:8 } }; +} + +function _slaArmorData(name) { + const armors = { + 'Downtown Jacket': { armorDice:1, armorAuto:0, cost:150, img:'icons/equipment/chest/vest-simple-leather-brown.webp' }, + 'CAF Armor': { armorDice:2, armorAuto:0, cost:400, img:'icons/equipment/chest/breastplate-banded-steel.webp' }, + 'Scout Armor': { armorDice:3, armorAuto:0, cost:800, img:'icons/equipment/chest/breastplate-layered-steel-grey.webp' }, + 'HARD Armor': { armorDice:4, armorAuto:0, cost:1500, img:'icons/equipment/chest/breastplate-scale-grey.webp' }, + 'Exo-Rig Armor': { armorDice:5, armorAuto:0, cost:3000, img:'icons/equipment/chest/breastplate-segmented-grey.webp' }, + 'Squad Shielded Power Armor': { armorDice:6, armorAuto:2, cost:8000, img:'icons/equipment/chest/breastplate-embossed-silver.webp' } + }; + const base = armors[name] || armors['Downtown Jacket']; + const { img, ...sys } = base; + return { name, type: 'armor', img: img || 'icons/svg/shield.svg', + system: { ...sys, description:'', armorRating: sys.armorDice, statMod:0, skillMod:0, statModTarget:'', skillModTarget:'', healthMod:0, resolveMod:0, initiativeMod:0, equipped:true } }; +} + +function _slaSpecialtyData(name) { + const specialties = { + 'Hard Hitter': { effects:'+1 Melee damage (sacrifice fast action)', img:'icons/skills/melee/strike-slashing-yellow.webp' }, + 'Fast Reflexes': { effects:'Draw 2 initiative cards, choose the better one', img:'icons/skills/movement/feet-winged-boots-glowing-yellow.webp' }, + 'Field Surgeon': { effects:'+1 Healing (critical injuries only)', img:'icons/tools/hand/pliers-pointed-red.webp' }, + 'Inquisitive': { effects:'Push WIT rolls twice per roll', img:'icons/skills/social/theft-pickpocket-eyes-green.webp' }, + 'Gut Feeling': { effects:'Use EMP for Observation (threat detection only)', img:'icons/magic/perception/eye-ringed-glow-angry-small-red.webp' }, + 'Sniper': { effects:'+2 Marksmanship (Long+ range, concealment required)', img:'icons/skills/ranged/aim-reticle-dark.webp' }, + 'True Grit': { effects:'+1 maximum Health', img:'icons/magic/life/heart-cross-strong-red.webp' }, + 'Ebb Sensitive': { effects:'Sense active Ebb users within ~10m radius', img:'icons/magic/perception/orb-eye-scrying.webp' }, + }; + const base = specialties[name] || { effects: name, img: 'icons/svg/aura.svg' }; + const { img, ...rest } = base; + return { name, type: 'specialty', img: img || 'icons/svg/aura.svg', + system: { ...rest, description:'', category:'general', prerequisites:'', package:'', healthMod:0, resolveMod:0, isActive:true } }; +} + +function _slaGearData(name) { + // Map common gear names to appropriate icons + const gearIcons = { + 'Medikit': 'icons/tools/medical/syringe-drug-blue.webp', + 'Medkit': 'icons/tools/medical/syringe-drug-blue.webp', + 'Comms': 'icons/tools/navigation/compass-brass.webp', + 'Radio': 'icons/tools/navigation/compass-brass.webp', + 'Binoculars': 'icons/tools/exploration/binoculars.webp', + 'Handcuffs': 'icons/tools/misc/chain-grey.webp', + 'Grapple': 'icons/tools/misc/rope-black.webp', + 'Rope': 'icons/tools/misc/rope-black.webp', + 'Flashlight': 'icons/tools/navigation/torch-black.webp', + 'Scanner': 'icons/tools/scribal/spectacles.webp', + 'Toolkit': 'icons/tools/hand/wrench.webp', + }; + const img = gearIcons[name] || 'systems/slazero2/assets/icons/cardboard-box-closed.svg'; + return { name, type: 'equipment', img, + system: { description:'', quantity:1, cost:0, weight:1, isDrug:false, active:false, activeDuration:'', activeLabel:'', activeNotes:'', equipped:true, initiativeMod:0 } }; +} + +// ══════════════════════════════════════════════════════════════════════════════ +// PC CREATOR APPLICATION +// ══════════════════════════════════════════════════════════════════════════════ +class SLAPCCreator extends Application { + constructor(options = {}) { + super(options); + this._step = 1; + this._race = null; + this._training = null; + this._attrs = {}; + this._skills = {}; + this._attrFreePoints = 0; + this._skillPoints = 0; + this._name = ''; + this._scl = 10; + this._credits = 500; + this._bio = ''; + } + + static get defaultOptions() { + return foundry.utils.mergeObject(super.defaultOptions, { + id: 'sla-pc-creator', + title: 'SLA Industries — Operative Generator', + template: null, + width: 680, + height: 620, + resizable: true, + classes: ['slazero2', 'sla-generator'] + }); + } + + async _renderInner() { + // Build full HTML inline — no external template needed + const el = document.createElement('div'); + el.innerHTML = this._buildHTML(); + return $(el); + } + + activateListeners(html) { + super.activateListeners(html); + this._bindStep1(html); + } + + _getEl(html, sel) { + return (html instanceof jQuery ? html[0] : html).querySelector(sel); + } + _getEls(html, sel) { + return [...(html instanceof jQuery ? html[0] : html).querySelectorAll(sel)]; + } + + // ── STEP 1: Race ──────────────────────────────────────────────────────────── + _bindStep1(html) { + this._showStep(html, 1); + this._getEls(html, '.race-card').forEach(card => { + card.addEventListener('click', () => { + this._getEls(html, '.race-card').forEach(c => c.classList.remove('selected')); + card.classList.add('selected'); + this._race = card.dataset.race; + this._getEl(html, '#gen-next-1').disabled = false; + }); + }); + this._getEl(html, '#gen-next-1').addEventListener('click', () => { + if (!this._race) return; + this._step = 2; + this._bindStep2(html); + }); + } + + // ── STEP 2: Training ──────────────────────────────────────────────────────── + _bindStep2(html) { + this._showStep(html, 2); + const raceData = SLA_RACES[this._race]; + this._getEls(html, '.training-card').forEach(card => { + const pkg = SLA_TRAINING[card.dataset.training]; + if (pkg.ebbOnly && !raceData.isEbbUser) { + card.classList.add('disabled'); + card.title = 'Requires Ebon or Brain Waster'; + return; + } + card.addEventListener('click', () => { + this._getEls(html, '.training-card:not(.disabled)').forEach(c => c.classList.remove('selected')); + card.classList.add('selected'); + this._training = card.dataset.training; + this._getEl(html, '#gen-next-2').disabled = false; + }); + }); + this._getEl(html, '#gen-back-2').addEventListener('click', () => { this._step = 1; this._bindStep1(html); }); + this._getEl(html, '#gen-next-2').addEventListener('click', () => { + if (!this._training) return; + // Init attributes from race + apply training skill bonus (done at creation) + const race = SLA_RACES[this._race]; + this._attrs = { ...race.baseAttrs }; + this._attrFreePoints = race.attrPoints; + // Apply training skill bonuses to skill pools + const trainSkills = { ...SLA_TRAINING[this._training].skills }; + this._skills = { force:0, melee:0, stamina:0, marksmanship:0, mobility:0, stealth:0, crafting:0, observation:0, survival:0, healing:0, insight:0, persuasion:0, ebb:0 }; + for (const [sk, val] of Object.entries(trainSkills)) { + this._skills[sk] = (this._skills[sk] || 0) + val; + } + this._skillPoints = race.skillPoints; + this._step = 3; + this._bindStep3(html); + }); + } + + // ── STEP 3: Attributes ────────────────────────────────────────────────────── + _bindStep3(html) { + this._showStep(html, 3); + this._renderAttrTable(html); + this._getEl(html, '#gen-back-3').addEventListener('click', () => { this._step = 2; this._bindStep2(html); }); + this._getEl(html, '#gen-next-3').addEventListener('click', () => { this._step = 4; this._bindStep4(html); }); + } + + _renderAttrTable(html) { + const race = SLA_RACES[this._race]; + const container = this._getEl(html, '#attr-rows'); + container.innerHTML = ''; + let remaining = this._attrFreePoints - Object.entries(this._attrs).reduce((acc, [k,v]) => acc + (v - race.baseAttrs[k]), 0); + + const render = () => { + remaining = this._attrFreePoints - Object.entries(this._attrs).reduce((acc, [k,v]) => acc + (v - race.baseAttrs[k]), 0); + this._getEl(html, '#attr-points-left').textContent = remaining; + this._getEl(html, '#gen-next-3').disabled = remaining < 0; + const hp = this._attrs.strength + this._attrs.agility + race.healthMod; + const rp = this._attrs.wits + this._attrs.empathy + race.resolveMod; + this._getEl(html, '#attr-hp-preview').textContent = hp; + this._getEl(html, '#attr-rp-preview').textContent = rp; + }; + + const ATTR_LABELS = { strength:'Strength (STR)', agility:'Agility (AGI)', wits:'Wits (WIT)', empathy:'Empathy (EMP)' }; + for (const [attr, baseVal] of Object.entries(race.baseAttrs)) { + const cap = race.attrCaps[attr]; + const row = document.createElement('div'); + row.className = 'attr-row'; + row.innerHTML = ` + ${ATTR_LABELS[attr]} + base ${baseVal} + + ${this._attrs[attr]} + + / ${cap}`; + container.appendChild(row); + row.querySelector('.attr-minus').addEventListener('click', () => { + if (this._attrs[attr] > baseVal) { this._attrs[attr]--; row.querySelector(`#av-${attr}`).textContent = this._attrs[attr]; render(); } + }); + row.querySelector('.attr-plus').addEventListener('click', () => { + const curRemaining = this._attrFreePoints - Object.entries(this._attrs).reduce((a,[k,v]) => a+(v-race.baseAttrs[k]),0); + if (this._attrs[attr] < cap && curRemaining > 0) { this._attrs[attr]++; row.querySelector(`#av-${attr}`).textContent = this._attrs[attr]; render(); } + }); + } + render(); + } + + // ── STEP 4: Skills ────────────────────────────────────────────────────────── + _bindStep4(html) { + this._showStep(html, 4); + this._renderSkillTable(html); + this._getEl(html, '#gen-back-4').addEventListener('click', () => { this._step = 3; this._bindStep3(html); }); + this._getEl(html, '#gen-next-4').addEventListener('click', () => { this._step = 5; this._bindStep5(html); }); + } + + _renderSkillTable(html) { + const race = SLA_RACES[this._race]; + const trainSkills = SLA_TRAINING[this._training].skills; + const SKILL_GROUPS = [ + { label:'Strength', attr:'STR', skills:['force','melee','stamina'] }, + { label:'Agility', attr:'AGI', skills:['marksmanship','mobility','stealth'] }, + { label:'Wits', attr:'WIT', skills:['crafting','observation','survival'] }, + { label:'Empathy', attr:'EMP', skills:['healing','insight','persuasion','ebb'] } + ]; + const container = this._getEl(html, '#skill-rows'); + container.innerHTML = ''; + let spent = 0; + + const render = () => { + spent = Object.entries(this._skills).reduce((a,[k,v]) => a + Math.max(0, v - (trainSkills[k]||0)), 0); + this._getEl(html, '#skill-points-left').textContent = Math.max(0, this._skillPoints - spent); + this._getEl(html, '#gen-next-4').disabled = spent > this._skillPoints; + }; + + for (const group of SKILL_GROUPS) { + const groupEl = document.createElement('div'); + groupEl.className = 'skill-group'; + groupEl.innerHTML = `
${group.label} (${group.attr})
`; + for (const sk of group.skills) { + if (sk === 'ebb' && !race.isEbbUser) continue; + const fromTraining = trainSkills[sk] || 0; + const row = document.createElement('div'); + row.className = 'skill-gen-row'; + row.innerHTML = ` + ${sk.charAt(0).toUpperCase()+sk.slice(1)} + ${fromTraining ? `+${fromTraining} training` : ''} + + ${this._skills[sk] || 0} + + / ${race.skillCap + fromTraining}`; + groupEl.appendChild(row); + row.querySelector('.sg-minus').addEventListener('click', () => { + const minVal = fromTraining; + if (this._skills[sk] > minVal) { this._skills[sk]--; row.querySelector(`#sv-${sk}`).textContent = this._skills[sk]; render(); } + }); + row.querySelector('.sg-plus').addEventListener('click', () => { + const curSpent = Object.entries(this._skills).reduce((a,[k,v]) => a + Math.max(0, v - (trainSkills[k]||0)), 0); + const cap = race.skillCap + fromTraining; + if (this._skills[sk] < cap && curSpent < this._skillPoints) { this._skills[sk]++; row.querySelector(`#sv-${sk}`).textContent = this._skills[sk]; render(); } + }); + } + container.appendChild(groupEl); + } + render(); + } + + // ── STEP 5: Details ───────────────────────────────────────────────────────── + _bindStep5(html) { + this._showStep(html, 5); + const nameInput = this._getEl(html, '#char-name'); + const sclInput = this._getEl(html, '#char-scl'); + const credInput = this._getEl(html, '#char-creds'); + const bioInput = this._getEl(html, '#char-bio'); + nameInput.value = this._name || _slaRandomName(this._race); + sclInput.value = this._scl; + credInput.value = this._credits; + bioInput.value = this._bio; + this._getEl(html, '#random-name-btn').addEventListener('click', () => { nameInput.value = _slaRandomName(this._race); }); + this._getEl(html, '#gen-back-5').addEventListener('click', () => { this._step = 4; this._bindStep4(html); }); + this._getEl(html, '#gen-next-5').addEventListener('click', () => { + this._name = nameInput.value.trim() || _slaRandomName(this._race); + this._scl = parseInt(sclInput.value) || 10; + this._credits = parseInt(credInput.value) || 500; + this._bio = bioInput.value; + this._step = 6; + this._bindStep6(html); + }); + } + + // ── STEP 6: Summary & Create ──────────────────────────────────────────────── + _bindStep6(html) { + this._showStep(html, 6); + const race = SLA_RACES[this._race]; + const training = SLA_TRAINING[this._training]; + const summary = this._getEl(html, '#summary-content'); + const attrStr = Object.entries(this._attrs).map(([k,v])=>`${k.slice(0,3).toUpperCase()} ${v}`).join(' | '); + const skillStr = Object.entries(this._skills).filter(([,v])=>v>0).map(([k,v])=>`${k.charAt(0).toUpperCase()+k.slice(1)} ${v}`).join(', ') || 'none'; + const hp = this._attrs.strength + this._attrs.agility + race.healthMod; + const rp = this._attrs.wits + this._attrs.empathy + race.resolveMod; + summary.innerHTML = ` +
Name${this._name}
+
Race${race.label}
+
Training${training.label}
+
SCL${this._scl}
+
Credits${this._credits}
+
Attributes${attrStr}
+
HP / Resolve${hp} / ${rp}
+
Skills${skillStr}
+
Specialty${training.specialty}
+
Armor${training.armor}
+
Weapons${(training.weapons||[]).join(', ') || '—'}
+ ${race.isEbbUser && (training.ebbEbon || training.ebb) ? (() => { + const list = this._race === 'ebon' && training.ebbEbon ? training.ebbEbon + : this._race === 'brainwaster' && training.ebbBrainwaster ? training.ebbBrainwaster + : (training.ebb || []); + const ebbVal = Number(this._skills.ebb) || 0; + return `
Starting Flux${Math.max(1, ebbVal + 1)}
+
Ebb Formulae (${list.length})${list.join(', ')}
`; + })() : ''}`; + this._getEl(html, '#gen-back-6').addEventListener('click', () => { this._step = 5; this._bindStep5(html); }); + this._getEl(html, '#gen-create-btn').addEventListener('click', () => this._createPC()); + } + + // ── CREATE ACTOR ───────────────────────────────────────────────────────────── + async _createPC() { + const race = SLA_RACES[this._race]; + const training = SLA_TRAINING[this._training]; + const hp = this._attrs.strength + this._attrs.agility + race.healthMod; + const rp = this._attrs.wits + this._attrs.empathy + race.resolveMod; + // Starting flux = Ebb skill + 1 (minimum 1 for Ebb users, 0 for non-Ebb) + const ebbSkillVal = Number(this._skills.ebb) || 0; + const flux = race.isEbbUser ? Math.max(1, ebbSkillVal + 1) : 0; + + // Build attributes in the nested object format the system expects + const attributes = {}; + for (const [k, v] of Object.entries(this._attrs)) { + attributes[k] = { value: v }; + } + // Build skills + const skills = {}; + for (const [k, v] of Object.entries(this._skills)) { + const ATTR_MAP = { force:'strength',melee:'strength',stamina:'strength',marksmanship:'agility',mobility:'agility',stealth:'agility',crafting:'wits',observation:'wits',survival:'wits',healing:'empathy',insight:'empathy',persuasion:'empathy',ebb:'empathy' }; + skills[k] = { value: v, attribute: ATTR_MAP[k], label: k.charAt(0).toUpperCase()+k.slice(1) }; + } + + const actorData = { + name: this._name, + type: 'character', + system: { + race: this._race, + archetype: this._training, + training: this._training, + details: { scl: this._scl, credits: this._credits, age: '', height: '', weight: '' }, + attributes, + skills, + derivedStats: { + health: { value: hp, max: hp }, + resolve: { value: rp, max: rp }, + broken: false + }, + stress: 0, + flux: { value: flux, max: flux }, + carryLimit: 4, + biography: this._bio, + specialties: training.specialty || '' + } + }; + + let actor; + try { + actor = await Actor.create(actorData); + } catch(err) { + ui.notifications.error(`Failed to create actor: ${err.message}`); + return; + } + + // Embed items: armor, weapons, specialty, gear, ebb formulae + const embeds = []; + if (training.armor) embeds.push(_slaArmorData(training.armor)); + for (const w of (training.weapons || [])) embeds.push(_slaWeaponData(w)); + if (training.specialty) embeds.push(_slaSpecialtyData(training.specialty)); + for (const g of (training.gear || [])) embeds.push(_slaGearData(g)); + + if (race.isEbbUser && (training.ebb || training.ebbEbon || training.ebbBrainwaster)) { + // Pick the race-appropriate formula list + let formulaList; + if (this._race === 'ebon' && training.ebbEbon) { + formulaList = training.ebbEbon; + } else if (this._race === 'brainwaster' && training.ebbBrainwaster) { + formulaList = training.ebbBrainwaster; + } else { + formulaList = training.ebb || []; + } + + // Build formulae from inline data (JSON packs are not compiled to LDB in Foundry v12+) + // Fall back to pack lookup only if the inline table is missing an entry. + const ebbPack = game.packs?.get('slazero2.ebb-formulae'); + let packIdx = null; + try { if (ebbPack) packIdx = await ebbPack.getIndex(); } catch(_) {} + + for (const formulaName of formulaList) { + const inline = _slaEbbFormulaData(formulaName); + if (inline) { + embeds.push(inline); + continue; + } + // Try world items as second fallback + const worldItem = game.items?.find(i => i.type === 'ebb' && i.name === formulaName); + if (worldItem) { embeds.push(worldItem.toObject()); continue; } + // Try compiled pack as last resort + if (packIdx) { + const entry = packIdx.find(e => e.name === formulaName); + if (entry) { + try { + const doc = await ebbPack.getDocument(entry._id); + embeds.push(doc.toObject()); + } catch(_) {} + } + } + } + } + + if (embeds.length) { + try { await actor.createEmbeddedDocuments('Item', embeds); } catch(e) { console.warn('SLA Zero Two | Could not embed starter items:', e); } + } + + ui.notifications.info(`✓ ${this._name} created as an Operative!`); + actor.sheet?.render(true); + this.close(); + } + + // ── UI UTILITIES ───────────────────────────────────────────────────────────── + _showStep(html, n) { + const root = html instanceof jQuery ? html[0] : html; + root.querySelectorAll('.gen-step').forEach(s => s.style.display = 'none'); + const step = root.querySelector(`#gen-step-${n}`); + if (step) step.style.display = 'flex'; + // Update progress bar + root.querySelectorAll('.gen-progress-dot').forEach((d, i) => { + d.classList.toggle('active', i < n); + d.classList.toggle('current', i === n - 1); + }); + } + + _buildHTML() { + const raceCards = Object.entries(SLA_RACES).map(([key, r]) => ` +
+
${r.icon}
+
${r.label}
+
STR${r.baseAttrs.strength} AGI${r.baseAttrs.agility} WIT${r.baseAttrs.wits} EMP${r.baseAttrs.empathy}
+
${r.flavor}
+ ${r.isEbbUser ? '
⚡ Ebb User
' : ''} +
`).join(''); + + const trainingCards = Object.entries(SLA_TRAINING).map(([key, t]) => ` +
+
${t.icon}
+
${t.label}
+
${t.flavor}
+
${Object.entries(t.skills).map(([s,v])=>`${s} +${v}`).join(', ')}
+ ${t.ebbOnly ? '
Ebb users only
' : ''} +
`).join(''); + + return ` +
+
+
Operative Generation — SLA Industries
+
${[1,2,3,4,5,6].map(n=>`
`).join('')}
+
+ + +
+
Step 1 — Select Race
+
${raceCards}
+
+ +
+
+ + + + + + + + + + + + + + + +
`; + } +} + +// ══════════════════════════════════════════════════════════════════════════════ +// NPC CREATOR APPLICATION +// ══════════════════════════════════════════════════════════════════════════════ +const NPC_PERSONALITIES = ['Aggressive','Calculating','Paranoid','Loyal','Cynical','Idealistic','Greedy','Fearless','Broken','Ruthless','Haunted','Fanatic']; +const NPC_MOTIVATIONS = ['Credits','Survival','Revenge','Ideology','Family','Fame','Fear of SLA','Promotion','Addiction','Love','Duty','Unknown']; +const NPC_ROLES = ['Enforcer','Informant','Black Market Dealer','Sector Gang Leader','Investigator','Corporate Spy','Ebb Cult Member','Renegade Operative','Cannibal Sector Survivor','Shiver Sergeant']; + +class SLANPCCreator extends Application { + constructor(options = {}) { + super(options); + this._mode = 'quick'; + } + + static get defaultOptions() { + return foundry.utils.mergeObject(super.defaultOptions, { + id: 'sla-npc-creator', + title: 'SLA Industries — NPC Generator', + template: null, + width: 560, + height: 580, + resizable: true, + classes: ['slazero2', 'sla-generator'] + }); + } + + async _renderInner() { + const el = document.createElement('div'); + el.innerHTML = this._buildHTML(); + return $(el); + } + + activateListeners(html) { + super.activateListeners(html); + const root = html instanceof jQuery ? html[0] : html; + + // Mode toggle + root.querySelectorAll('.mode-tab').forEach(tab => { + tab.addEventListener('click', () => { + root.querySelectorAll('.mode-tab').forEach(t => t.classList.remove('active')); + tab.classList.add('active'); + this._mode = tab.dataset.mode; + root.querySelector('#full-fields').style.display = this._mode === 'full' ? 'block' : 'none'; + }); + }); + + // Random fill button + root.querySelector('#npc-randomize').addEventListener('click', () => this._randomFill(root)); + + // Create button + root.querySelector('#npc-create-btn').addEventListener('click', () => this._createNPC(root)); + + // Threat slider → update label + const threatSlider = root.querySelector('#npc-threat'); + const threatLabel = root.querySelector('#npc-threat-label'); + if (threatSlider) { + threatSlider.addEventListener('input', () => { threatLabel.textContent = threatSlider.value; }); + } + } + + _randomFill(root) { + // Pick random archetype + const types = Object.keys(SLA_NPC_TYPES); + const key = types[Math.floor(Math.random() * types.length)]; + root.querySelector('#npc-archetype').value = key; + + const archetype = SLA_NPC_TYPES[key]; + root.querySelector('#npc-threat').value = archetype.threat; + root.querySelector('#npc-threat-label').textContent = archetype.threat; + + // Random name + const nameTable = archetype.isEbb ? 'ebon' : 'generic'; + root.querySelector('#npc-name').value = _slaRandomName(nameTable); + + if (this._mode === 'full') { + root.querySelector('#npc-personality').value = NPC_PERSONALITIES[Math.floor(Math.random()*NPC_PERSONALITIES.length)]; + root.querySelector('#npc-motivation').value = NPC_MOTIVATIONS[Math.floor(Math.random()*NPC_MOTIVATIONS.length)]; + root.querySelector('#npc-role').value = NPC_ROLES[Math.floor(Math.random()*NPC_ROLES.length)]; + root.querySelector('#npc-notes').value = `${root.querySelector('#npc-name').value} is a ${root.querySelector('#npc-personality').value.toLowerCase()} ${archetype.label.toLowerCase()} driven by ${root.querySelector('#npc-motivation').value.toLowerCase()}.`; + } + } + + async _createNPC(root) { + const archetypeKey = root.querySelector('#npc-archetype').value; + const archetype = SLA_NPC_TYPES[archetypeKey]; + const threat = parseInt(root.querySelector('#npc-threat').value) || archetype.threat; + let npcName = root.querySelector('#npc-name').value.trim(); + + if (!npcName) npcName = `${archetype.label} #${Math.floor(Math.random()*900+100)}`; + + // Scale stats by threat delta + const delta = threat - archetype.threat; + const attrs = {}; + for (const [k, v] of Object.entries(archetype.attrs)) { + attrs[k] = Math.max(2, Math.min(5, v + delta)); + } + const hp = archetype.hp + delta * 2; + const armorVal = Math.max(0, Math.min(6, archetype.armor + delta)); + + // Build notes + let notes = ''; + if (this._mode === 'full') { + const personality = root.querySelector('#npc-personality')?.value || ''; + const motivation = root.querySelector('#npc-motivation')?.value || ''; + const role = root.querySelector('#npc-role')?.value || ''; + const extraNotes = root.querySelector('#npc-notes')?.value || ''; + if (personality || motivation || role) { + notes = `Personality: ${personality}\nMotivation: ${motivation}\nRole: ${role}\n\n${extraNotes}`; + } else { + notes = extraNotes; + } + } + + // NPC uses simplified system fields + const actorData = { + name: npcName, + type: 'npc', + system: { + biography: notes, + threat, + attributes: { ...attrs }, + derivedStats: { + health: { value: hp, max: hp }, + resolve: { value: Math.max(2, attrs.wits + attrs.empathy), max: Math.max(2, attrs.wits + attrs.empathy) } + }, + skills: { ...archetype.skills }, + armor: armorVal, + damage: Math.max(2, attrs.strength), + flux: archetype.flux ? { value: archetype.flux, max: archetype.flux } : { value: 0, max: 0 } + } + }; + + let actor; + try { + actor = await Actor.create(actorData); + } catch(err) { + ui.notifications.error(`Failed to create NPC: ${err.message}`); + return; + } + + // Embed weapons + const embeds = []; + for (const w of (archetype.weapons || [])) { + if (w !== 'claws') embeds.push(_slaWeaponData(w)); + } + if (embeds.length) { + try { await actor.createEmbeddedDocuments('Item', embeds); } catch(_) {} + } + + // Threat descriptor + const threatNames = {1:'Mook',2:'Standard',3:'Tough',4:'Elite',5:'Boss',6:'Apex'}; + ui.notifications.info(`✓ ${npcName} (${archetype.label} — Threat ${threat}: ${threatNames[threat] || 'Unknown'}) created.`); + actor.sheet?.render(true); + this.close(); + } + + _buildHTML() { + const archetypeOptions = Object.entries(SLA_NPC_TYPES).map(([key, a]) => + ``).join(''); + + return ` +
+
+
NPC Generator — SLA Industries
+
+
⚡ Quick
+
📋 Full
+
+
+ +
+
+ + +
+
+ + + 1=Mook · 3=Tough · 5=Boss · 6=Apex +
+
+ + +
+ + + +
+ +
+ + +
+
`; + } +} + +// ══════════════════════════════════════════════════════════════════════════════ +// SLA GM FINANCE LEDGER +// GM-only tool: lists all Player Character actors, shows current balance, +// weekly income/expenses, net weekly. +// Calculate button: newCredits = credits + income - expenses, then zeros +// all income and expense fields on the actor. +// ══════════════════════════════════════════════════════════════════════════════ + +class SLAGMFinanceTool extends Application { + constructor(...args) { + super(...args); + this._actorHookId = null; + } + + static get defaultOptions() { + return foundry.utils.mergeObject(super.defaultOptions, { + id: 'sla-gm-finance-tool', + title: 'GM Finance Ledger', + width: 680, + height: 'auto', + resizable: true, + classes: ['slazero2', 'gm-finance-tool'] + }); + } + + // ── Auto-refresh when any actor is updated ──────────────────────────────── + async _render(force, options) { + await super._render(force, options); + if (!this._actorHookId) { + this._actorHookId = Hooks.on('updateActor', () => { + if (this.rendered) this.render(false); + }); + } + } + + async close(options) { + if (this._actorHookId) { + Hooks.off('updateActor', this._actorHookId); + this._actorHookId = null; + } + return super.close(options); + } + + // ── Read credits from the actor's raw source data ───────────────────────── + // _source bypasses any DataModel computed values and gives the raw DB value. + _readCredits(actor) { + let raw = actor._source?.system?.details?.credits; + if (raw === undefined || raw === null) + raw = actor.system?.details?.credits; + if (raw === undefined || raw === null) + raw = foundry.utils.getProperty(actor, 'system.details.credits'); + // Fix "500,500" string from duplicate-named form inputs + if (typeof raw === 'string' && raw.includes(',')) + raw = raw.split(',')[0]; + const n = Number(raw); + return isNaN(n) ? 0 : n; + } + + // ── Read a finances sub-value safely ───────────────────────────────────── + _readFinance(actor, path) { + let raw = foundry.utils.getProperty(actor._source ?? {}, `system.${path}`) + ?? foundry.utils.getProperty(actor, `system.${path}`); + const n = Number(raw); + return isNaN(n) ? 0 : n; + } + + // ── Build per-actor data row ────────────────────────────────────────────── + _getPCData() { + return (game.actors ?? []) + .filter(a => a.type === 'character' && a.system?.details?.isPlayerCharacter) + .map(a => { + const salary = this._readFinance(a, 'finances.income.salary'); + const bpn = this._readFinance(a, 'finances.income.bpnReward'); + const incOther = this._readFinance(a, 'finances.income.other'); + const accom = this._readFinance(a, 'finances.expenses.accommodation'); + const drugs = this._readFinance(a, 'finances.expenses.drugs'); + const subs = this._readFinance(a, 'finances.expenses.subscriptions'); + const expOther = this._readFinance(a, 'finances.expenses.other'); + const btax = this._readFinance(a, 'finances.expenses.bulletTax'); + const weeklyIncome = salary + bpn + incOther; + const weeklyExpenses = accom + drugs + subs + expOther + btax; + const netWeekly = weeklyIncome - weeklyExpenses; + const credits = this._readCredits(a); + return { + id: a.id, name: a.name, + credits, weeklyIncome, weeklyExpenses, netWeekly + }; + }); + } + + // ── Apply week: credits += income - expenses, then zero all fields ───────── + async _applyWeek(actorId) { + if (!game.user?.isGM) return; + const actor = game.actors.get(actorId); + if (!actor) return; + + const salary = this._readFinance(actor, 'finances.income.salary'); + const bpn = this._readFinance(actor, 'finances.income.bpnReward'); + const incOther = this._readFinance(actor, 'finances.income.other'); + const accom = this._readFinance(actor, 'finances.expenses.accommodation'); + const drugs = this._readFinance(actor, 'finances.expenses.drugs'); + const subs = this._readFinance(actor, 'finances.expenses.subscriptions'); + const expOther = this._readFinance(actor, 'finances.expenses.other'); + const btax = this._readFinance(actor, 'finances.expenses.bulletTax'); + + const income = salary + bpn + incOther; + const expenses = accom + drugs + subs + expOther + btax; + const current = this._readCredits(actor); + const newBal = current + income - expenses; + + // {render: false} stops Foundry from auto-submitting open character sheets + // before they re-render, which was causing stale form values (e.g. 500) + // to be merged with the new value (e.g. 852) into a corrupt "852,500" string. + await actor.update({ + 'system.details.credits': newBal, + 'system.finances.income.salary': 0, + 'system.finances.income.bpnReward': 0, + 'system.finances.income.other': 0, + 'system.finances.expenses.accommodation': 0, + 'system.finances.expenses.drugs': 0, + 'system.finances.expenses.subscriptions': 0, + 'system.finances.expenses.other': 0, + 'system.finances.expenses.bulletTax': 0, + 'system.finances.ammoSpentSession': 0, + 'system.finances.debt': 0 + }, { render: false }); + + // Now re-render any open sheets for this actor cleanly from fresh data. + for (const app of Object.values(actor.apps ?? {})) { + app.render(false); + } + + ui.notifications.info( + `${actor.name}: ${current}c + ${income}c income − ${expenses}c expenses = ${newBal}c` + ); + } + + // ── Render the table ────────────────────────────────────────────────────── + async _renderInner(_data) { + const pcs = this._getPCData(); + let rows = ''; + + if (pcs.length === 0) { + rows = ` + No Player Characters found.
+ Tick Player Character on each actor's Biography tab. + `; + } else { + for (const pc of pcs) { + const netClass = pc.netWeekly >= 0 ? 'net-positive' : 'net-negative'; + const netSign = pc.netWeekly >= 0 ? '+' : ''; + const newBal = pc.credits + pc.netWeekly; + rows += ` + + ${pc.name} + ${pc.credits}c + ${pc.weeklyIncome}c + ${pc.weeklyExpenses}c + ${netSign}${pc.netWeekly}c + + + + `; + } + } + + const html = ` +
+
+ + Calculate: credits = balance + income − expenses. + All income & expense fields are then reset to 0. +
+ + + + + + + + + + + + ${rows} +
CharacterBalanceIncomeExpensesNet
+ +
`; + + return $(html); + } + + activateListeners(html) { + super.activateListeners(html); + const root = html instanceof jQuery ? html[0] : html; + + root.querySelectorAll('.gm-finance-calc-btn').forEach(btn => { + btn.addEventListener('click', async () => { + btn.disabled = true; + await this._applyWeek(btn.dataset.actorId); + // render() fires automatically via the updateActor hook + }); + }); + + root.querySelector('.gm-finance-calc-all-btn')?.addEventListener('click', async () => { + const pcs = this._getPCData(); + for (const pc of pcs) await this._applyWeek(pc.id); + }); + } +} + +// ══════════════════════════════════════════════════════════════════════════════ +// SLA GROUP NPC GENERATOR +// GM tool: quickly generate and track groups of Carrions, Serial Killers, +// Monstarets or Machines. Compact editable stat cards, HP tracking, +// reroll per card, and one-click export to World actors. +// ══════════════════════════════════════════════════════════════════════════════ + +const SLA_NPC_GROUP_ARCHETYPES = { + + streetgang: { + label: 'Street Gang', color: '#cc8800', icon: 'fas fa-street-view', + nameParts: ['','','','Big','Mad','Dead','Crazy','One-Eye'], + nameBase: ['Sloper','Ganger','Thug','Punk','Ratter','Knife-Boy','Street Dog','Scrapper','Slum Rat'], + stats: { str:[2,3], agi:[2,3], wit:[1,2], emp:[1,3] }, + health:[2,4], armor:[0,2], damage:[1,3], threat:[1,2], + armorPool:[ + 'None','None','None', + 'Street Leathers (ARM 1)','Street Leathers (ARM 1)', + 'Padded Jacket (ARM 1)', + 'Scavenged Vest (ARM 2)', + 'Old Combat Jacket (ARM 2)', + ], + weaponPool:[ + // Slot 1 — always a melee weapon + [ + {name:'Lead Pipe', damage:2,ap:0,range:'Engaged'}, + {name:'Knife', damage:1,ap:1,range:'Engaged'}, + {name:'Baseball Bat', damage:2,ap:0,range:'Engaged'}, + {name:'Crowbar', damage:2,ap:1,range:'Engaged'}, + {name:'Machete', damage:2,ap:1,range:'Engaged'}, + {name:'Broken Bottle',damage:1,ap:0,range:'Engaged'}, + {name:'Chain', damage:1,ap:0,range:'Engaged'}, + {name:'Hatchet', damage:2,ap:1,range:'Engaged'}, + {name:'Knuckleduster',damage:1,ap:0,range:'Engaged',note:'bonus: free grab on hit'}, + ], + // Slot 2 — 40% chance of a ranged weapon (nulls reduce probability) + [ + null,null,null,null,null,null, + {name:'Cheap Pistol', damage:2,ap:0,range:'Short'}, + {name:'Sawn-Off Shotgun', damage:3,ap:0,range:'Engaged'}, + {name:'Crossbow', damage:2,ap:1,range:'Medium'}, + {name:'Cheap SMG', damage:2,ap:0,range:'Short',note:'ROF 3'}, + ], + ], + abilities:[ + 'Gang Up — when 2+ attack same target, one gets +1 die', + 'Street Smarts — +1 Observation die in urban environments', + 'Intimidation — Force roll success triggers Panic check in civilians', + 'Mob Mentality — +1 die when within 5m of 2+ allies', + 'Dirty Fighter — free grab attempt on any melee hit', + 'Numbers Game — group of 5+ gains +2 morale threshold', + 'Scavenger — minor action: grab a nearby dropped weapon or item', + ] + }, + + carrion: { + label: 'Carrion', color: '#cc4422', icon: 'fas fa-biohazard', + nameParts: ['Bloated','Feral','Stalking','Gnawing','Lurching','Rabid','Rotting','Hulking',''], + nameBase: ['Carrion','Carrion','Carrien','Feeder','Gnawer','Pack Carrion','Shambler'], + stats: { str:[2,4], agi:[1,2], wit:[1,1], emp:[1,1] }, + health:[3,6], armor:[0,1], damage:[2,3], threat:[1,2], + armorPool:['None','None','None','Matted Flesh (ARM 1)'], + naturalWeapons:[ + {name:'Bite', damage:2, ap:1, note:'ignores 1 armour; target STR check or Bleeding'}, + {name:'Claws', damage:1, ap:0, note:'always counts as armed; can rake two targets'}, + ], + abilities:[ + 'Pack Aggression — +1 die per Carrion adjacent to the same target', + 'Mindless — immune to Panic and all psychological/social effects', + 'Hard to Kill — rolls Physical Critical even at 0 HP (once per fight)', + 'Swarm — 3+ Carrion attacking same target simultaneously: +2 damage total', + 'Contagion — bite victims must pass STR check or contract Rot Fever', + 'Shamble — moves half speed but ignores difficult terrain', + 'Death Rattle — on death, adjacent targets must make a Morale check', + ] + }, + + serialkiller: { + label: 'Serial Killer', color: '#882244', icon: 'fas fa-skull', + nameParts: ['The','The','The',''], + nameBase: ['Butcher','Flenser','Ghost','Surgeon','Reaper','Cutter','Shadow','Wraith','Stalker','Hollow','Hunter'], + stats: { str:[2,4], agi:[2,4], wit:[2,3], emp:[1,2] }, + health:[3,5], armor:[0,2], damage:[3,4], threat:[3,4], + armorPool:[ + 'None','None','None', + 'Leather Coat (ARM 1)','Leather Coat (ARM 1)', + 'Tactical Vest (ARM 2)', + 'Scavenged Body Armour (ARM 2)', + ], + weaponPool:[ + // Slot 1 — signature melee weapon (always present) + [ + {name:'Hunting Knife', damage:2,ap:1,range:'Engaged'}, + {name:'Butcher Cleaver', damage:3,ap:0,range:'Engaged'}, + {name:'Surgical Scalpel', damage:2,ap:2,range:'Engaged',note:'ignores 2 ARM'}, + {name:'Combat Knife', damage:2,ap:1,range:'Engaged'}, + {name:'Chainsaw', damage:4,ap:1,range:'Engaged',note:'Loud — breaks Stealth'}, + {name:'Hatchet', damage:3,ap:1,range:'Engaged'}, + {name:'Piano Wire', damage:2,ap:0,range:'Engaged',note:'Grapple: auto 2 DMG/turn'}, + {name:'Bone Saw', damage:3,ap:2,range:'Engaged'}, + {name:'Stiletto', damage:2,ap:2,range:'Engaged',note:'ignores soft armour'}, + ], + // Slot 2 — 30% chance of secondary ranged weapon + [ + null,null,null,null,null,null,null, + {name:'Pistol', damage:2,ap:0,range:'Short'}, + {name:'Sniper Rifle',damage:3,ap:2,range:'Long',note:'ignores cover'}, + {name:'Crossbow', damage:2,ap:1,range:'Medium',note:'Silent'}, + ], + ], + abilities:[ + 'Signature Weapon — +1 die when using preferred weapon type', + 'Predator Focus — ignores cover against a chosen target', + 'Surprise Strike — first attack each combat: +2 damage', + 'Psychotic Endurance — ignores one level of injury penalties', + 'Brutal Efficiency — 3+ successes on attack: target is Stunned', + 'Stealth — 3 free Stealth dice when ambushing from concealment', + 'Trophies — each kill this session: cumulative +1 to next damage roll', + 'Evasion — once per round: negate one melee hit on a 4+', + ] + }, + + monstaret: { + label: 'Monstaret', color: '#226622', icon: 'fas fa-dragon', + // Subtypes define the full stat profile, natural weapons and abilities + subtypes:[ + { + name:'Fang Beast', + modifiers:['Alpha ','Juvenile ','Mutant ',''], + stats:{str:[4,5],agi:[3,4],wit:[1,1],emp:[1,1]}, + health:[7,10], armor:[1,2], damage:[3,4], threat:[3,4], + naturalWeapons:[ + {name:'Bite', damage:3,ap:2,note:'target STR check or Bleeding (1 DMG/turn)'}, + {name:'Claw Swipe', damage:2,ap:0,note:'can hit 2 adjacent targets in one action'}, + ], + abilities:[ + 'Pack Hunter — +1 die when 2+ Fang Beasts are present', + 'Pounce — charge up to 10m as a free move before attacking', + 'Bleeding Bite — on hit: target gains Bleeding condition (1 DMG/turn)', + 'Frenzy — at ≤ half HP, gains an additional attack per round', + ] + }, + { + name:'Rend Beast', + modifiers:['Ancient ','Elder ','Corrupted ',''], + stats:{str:[5,6],agi:[1,2],wit:[1,1],emp:[1,1]}, + health:[9,13], armor:[2,4], damage:[4,5], threat:[4,5], + naturalWeapons:[ + {name:'Rending Claws',damage:4,ap:1,note:'3+ successes: Rend — ongoing 1 DMG/turn'}, + {name:'Crushing Grip',damage:2,ap:0,note:'target Grappled; STR vs STR to escape'}, + ], + abilities:[ + 'Unstoppable — not stopped by barriers; pushes through obstacles', + 'Rending Strike — 3+ successes: target gains Bleeding condition', + 'Toughened Hide — ignore first 1 damage from each attack', + 'Slow — only moves Short range per turn but cannot be tripped', + ] + }, + { + name:'Pit-Thing', + modifiers:['Corrupted ','Mutant ','',''], + stats:{str:[3,4],agi:[3,4],wit:[1,1],emp:[1,1]}, + health:[5,8], armor:[1,2], damage:[2,3], threat:[3,4], + naturalWeapons:[ + {name:'Acid Spit', damage:2,ap:3,note:'range Short; hit: −1 ARM permanently'}, + {name:'Bite', damage:2,ap:1,note:'acid contact: attacker takes 1 DMG on hit'}, + ], + abilities:[ + 'Acid Resistance — immune to acid and corrosive effects', + 'Wall Crawl — moves freely on vertical and overhead surfaces', + 'Spit Barrage — acid spit at 2 targets per round (−1 die each)', + 'Dissolve Armour — each acid hit permanently reduces target ARM by 1', + ] + }, + { + name:'Lurker', + modifiers:['Shadow ','Deep ','',''], + stats:{str:[3,4],agi:[3,5],wit:[1,2],emp:[1,1]}, + health:[5,8], armor:[0,2], damage:[2,3], threat:[3,4], + naturalWeapons:[ + {name:'Tentacle Lash',damage:2,ap:0,note:'reach 5m; 3+ successes: target Grappled'}, + {name:'Constrict', damage:3,ap:0,note:'only vs Grappled targets; auto each turn'}, + ], + abilities:[ + 'Camouflage — 4 free Stealth dice; invisible unless moving', + 'Ambush — first attack from hidden: +3 dice', + 'Multi-Grab — can Grapple up to 3 targets (one tentacle each)', + 'Squeeze — Constrict deals +1 DMG per round target remains Grappled', + ] + }, + { + name:'Howler', + modifiers:['Alpha ','Pack ','',''], + stats:{str:[3,4],agi:[4,5],wit:[1,1],emp:[1,1]}, + health:[4,7], armor:[0,1], damage:[2,3], threat:[3,4], + naturalWeapons:[ + {name:'Sonic Screech',damage:2,ap:0,note:'all in Short range; 2+ successes: Stunned'}, + {name:'Claw', damage:2,ap:0,note:'fast — can attack twice per round'}, + ], + abilities:[ + 'Screech — area attack within Short range; all targets check vs Stun', + 'Speed — always acts initiative phase 1; attacks twice per round', + 'Pack Call — if alive after round 1, summons 1d3 additional Howlers', + 'Fragile — only ARM 0–1 but extremely agile and hard to hit', + ] + }, + { + name:'Crawler', + modifiers:['Giant ','Armoured ','Brood ',''], + stats:{str:[4,5],agi:[2,3],wit:[1,1],emp:[1,1]}, + health:[7,11], armor:[2,4], damage:[3,4], threat:[3,4], + naturalWeapons:[ + {name:'Mandibles',damage:3,ap:1,note:'STR check or Paralysed for 1 round'}, + {name:'Leg Spike',damage:2,ap:0,note:'free attack against rear arc targets'}, + ], + abilities:[ + 'Predatory Instinct — always acts in initiative phase 1', + 'Paralytic Bite — on hit: target STR check or Paralysed 1 round', + 'Carapace — ARM 2 on top/sides; 0 from below; called shots can bypass', + 'Many-Legged — immune to trip/knockdown effects', + ] + }, + ] + }, + + machine: { + label: 'Machine', color: '#334488', icon: 'fas fa-robot', + subtypes:[ + { + name:'Combat Unit', + designation:['MK-','Unit-',''], + stats:{str:[4,5],agi:[2,3],wit:[3,4],emp:[0,0]}, + health:[6,10], armor:[3,5], damage:[3,4], threat:[3,4], + integratedWeapons:[ + {name:'Integrated Rifle', damage:3,ap:1,range:'Long', note:'ROF 1'}, + {name:'Combat Blade', damage:3,ap:1,range:'Engaged'}, + ], + abilities:[ + 'Targeting — re-roll one miss die per attack', + 'Armoured Shell — natural ARM 3 (already in stats)', + 'Threat Assessment — always knows current HP of visible targets', + 'Suppression Protocol — target that failed to hit: −1 die next attack', + ] + }, + { + name:'Scout Drone', + designation:['DES-','SD-',''], + stats:{str:[2,3],agi:[4,5],wit:[3,4],emp:[0,0]}, + health:[3,5], armor:[1,2], damage:[2,3], threat:[2,3], + integratedWeapons:[ + {name:'Shock Taser', damage:2,ap:0,range:'Engaged',note:'Stunned on hit'}, + {name:'Dart Pistol', damage:1,ap:0,range:'Short', note:'sedative: STR check or unconscious'}, + ], + abilities:[ + 'Flight — ignores ground terrain; moves in full 3D space', + 'Sensor Suite — detects all living targets within 20m regardless of cover', + 'Relay — allies gain +1 die against targets the drone has acquired', + 'Emergency Protocol — at ≤2 HP: transmits distress signal (reinforcements in 3 rounds)', + ] + }, + { + name:'Heavy Assault Unit', + designation:['HAU-','MK-',''], + stats:{str:[5,6],agi:[1,2],wit:[2,3],emp:[0,0]}, + health:[9,14], armor:[5,7], damage:[4,6], threat:[4,5], + integratedWeapons:[ + {name:'Minigun', damage:3,ap:0,range:'Medium',note:'ROF 6; suppression-fire area attack'}, + {name:'Flamethrower', damage:4,ap:2,range:'Short', note:'On Fire condition; Short cone area'}, + {name:'Armour Fist', damage:5,ap:2,range:'Engaged',note:'knockback 5m on hit'}, + ], + abilities:[ + 'Weapon Array — fires two weapons at different targets simultaneously', + 'Overload — on death: 4 damage to all within 5m', + 'Juggernaut — immune to Grapple; ignores terrain and barriers', + 'Targeting Computer — re-rolls all miss dice once per attack', + ] + }, + { + name:'Maintenance Unit', + designation:['MU-','SYS-',''], + stats:{str:[3,4],agi:[2,3],wit:[3,4],emp:[0,0]}, + health:[4,7], armor:[2,3], damage:[2,3], threat:[2,3], + integratedWeapons:[ + {name:'Cutting Laser',damage:2,ap:3,range:'Engaged',note:'fully ignores armour AP'}, + {name:'Shock Wrench', damage:2,ap:0,range:'Engaged',note:'EMP: short-circuits electronics'}, + ], + abilities:[ + 'Field Repairs — restore 2 HP to another Machine unit as a full action', + 'Structural Knowledge — +2 AP vs armoured targets (joint strikes)', + 'EMP Pulse — once per combat: all electronics within 5m offline for 1 round', + 'Diagnostic Mode — immune to Blinded and Stunned conditions', + ] + }, + ] + } +}; + +class SLAGroupNPCTool extends Application { + constructor(...args) { + super(...args); + this._npcs = []; + this._selectedType = 'carrion'; + this._count = 4; + this._root = null; + this._threatLevel = 3; // 1 = baseline, 10 = elite. Scale: AGI/initiative focus. + } + + static get defaultOptions() { + return foundry.utils.mergeObject(super.defaultOptions, { + id: 'sla-group-npc-tool', + title: 'Group NPC Generator', + width: 860, + height: 600, + resizable: true, + classes: ['slazero2', 'group-npc-tool'] + }); + } + + // ── Utilities ───────────────────────────────────────────────────────────── + _rng(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; } + _pick(arr) { return arr[Math.floor(Math.random() * arr.length)]; } + _threatLevelLabel(lvl) { + const names = ['','Baseline','Quick','Alert','Dangerous','Veteran','Hardened','Elite','Apex','Legendary','Mythic']; + return `${lvl} — ${names[lvl] ?? lvl}`; + } + _hexRgb(hex) { + const r = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); + return r ? `${parseInt(r[1],16)},${parseInt(r[2],16)},${parseInt(r[3],16)}` : '128,128,128'; + } + _esc(s) { return String(s).replace(/&/g,'&').replace(/"/g,'"').replace(/ Math.random() - 0.5); + abilities = pool.slice(0, 2); + armorDesc = `Built-in ARM ${armor}`; + } else { + // --- Standard flat generation (Street Gang, Carrion, Serial Killer) --- + const prefix = this._pick(arch.nameParts); + const base = this._pick(arch.nameBase); + name = prefix ? `${prefix}${prefix.endsWith('-') ? '' : ' '}${base}` : base; + + str = this._rng(...arch.stats.str); + agi = this._rng(...arch.stats.agi); + wit = this._rng(...arch.stats.wit); + emp = this._rng(...arch.stats.emp); + hpMax = this._rng(...arch.health); + armor = this._rng(...arch.armor); + damage = this._rng(...arch.damage); + threat = this._rng(...arch.threat); + + if (arch.naturalWeapons) naturalWeapons = [...arch.naturalWeapons]; + + if (arch.weaponPool) { + for (const slot of arch.weaponPool) { + const pick = this._pick(slot); + if (pick) weapons.push(pick); + } + } + + const pool = [...(arch.abilities ?? [])].sort(() => Math.random() - 0.5); + abilities = pool.slice(0, 2); + + if (arch.armorPool) { + const ap = this._pick(arch.armorPool); + armorDesc = ap ?? 'None'; + } else { + armorDesc = armor > 0 ? `ARM ${armor}` : 'None'; + } + } + + const npc = { + id: foundry.utils.randomID(), typeKey, + typeLabel: arch.label, + color: arch.color, icon: arch.icon, + name, str, agi, wit, emp, + hp: hpMax, hpMax, armor, damage, threat, + weapons, naturalWeapons, armorDesc, + abilities, notes: '', defeated: false + }; + return this._applyThreatBonus(npc); + } + + // ── Threat-level scaling ─────────────────────────────────────────────────── + // Scales AGI (initiative) as the primary target; STR, HP, ARM, DMG also + // creep up at higher levels. Level 1 = baseline (no stat changes). + // initPool is ALWAYS set: max(5, agi) + threatLevel gives at least 6d6 at + // level 1 so every NPC can match players on initiative. + _applyThreatBonus(npc) { + const lvl = Math.max(1, Math.min(10, this._threatLevel ?? 1)); + npc.threatBonus = lvl; + + if (lvl > 1) { + // Per-level bonus table: [agiBonus, strBonus, witBonus, hpPct, armorBonus, dmgBonus] + const TABLE = [ + [0, 0, 0, 0.00, 0, 0], // 1 — baseline (unused; only entered for lvl > 1) + [1, 0, 0, 0.00, 0, 0], // 2 — quick + [1, 0, 1, 0.00, 0, 0], // 3 — alert + [2, 1, 1, 0.00, 0, 0], // 4 — dangerous + [2, 1, 1, 0.25, 0, 1], // 5 — veteran + [3, 2, 1, 0.25, 1, 1], // 6 — hardened + [3, 2, 2, 0.50, 1, 1], // 7 — elite + [4, 3, 2, 0.50, 1, 2], // 8 — apex + [4, 3, 3, 0.75, 2, 2], // 9 — legendary + [5, 4, 3, 1.00, 2, 3], // 10 — mythic + ]; + const [agiB, strB, witB, hpPct, armB, dmgB] = TABLE[lvl - 1]; + + npc.agi = Math.min(8, npc.agi + agiB); + npc.str = Math.min(8, npc.str + strB); + npc.wit = Math.min(8, npc.wit + witB); + npc.armor = Math.min(8, npc.armor + armB); + npc.damage = Math.min(8, npc.damage + dmgB); + + if (hpPct > 0) { + const extra = Math.ceil(npc.hpMax * hpPct); + npc.hpMax += extra; + npc.hp += extra; + } + } + + // Initiative dice pool: base AGI (floor 5) + one bonus die per threat level. + // A level-1 Carrion (AGI 2) → max(2,5)+1 = 6d6; level-3 Carrion → max(3,5)+3 = 8d6. + // This guarantees NPCs can contest player initiative at all threat tiers. + npc.initPool = Math.max(5, npc.agi) + lvl; + return npc; + } + + // ── Icon helpers — SLA-specific art ───────────────────────────────────── + static _SLA = 'systems/slazero2/assets/npc-art'; + + /** Portrait icon for the NPC actor based on type */ + _getNPCIcon(typeKey) { + const p = SLAGroupNPCTool._SLA + '/portraits'; + return { + streetgang: `${p}/streetgang.png`, + carrion: `${p}/carrion.png`, + serialkiller: `${p}/serialkiller.png`, + monstaret: `${p}/monstaret.png`, + machine: `${p}/machine.png`, + }[typeKey] ?? `${p}/enemy-generic.png`; + } + + /** Choose weapon item icon from SLA art */ + _getWeaponIcon(weapon) { + const n = (weapon.name || '').toLowerCase(); + const r = (weapon.range || '').toLowerCase(); + const W = SLAGroupNPCTool._SLA + '/weapons'; + // Natural + if (n.includes('bite') || n.includes('mandible') || n.includes('constrict') || n.includes('grip')) + return `${W}/natural-weapon.png`; + if (n.includes('claw') || n.includes('rend') || n.includes('tentacle') || n.includes('talon')) + return `${W}/claws.png`; + if (n.includes('spit') || n.includes('acid') || n.includes('screech') || n.includes('sonic')) + return 'icons/svg/aura.svg'; + // Ranged + if (n.includes('sniper') || n.includes('marksman')) + return `${W}/sniper.png`; + if (n.includes('rifle') || n.includes('minigun') || n.includes('autocannon') || n.includes('lmg') || n.includes('flamethrower')) + return `${W}/rifle.png`; + if (n.includes('shotgun') || n.includes('street sweeper') || n.includes('sawn')) + return `${W}/shotgun.png`; + if (n.includes('smg') || n.includes('submachine')) + return `${W}/smg.png`; + if (n.includes('pistol') || n.includes('handgun') || n.includes('dart') || n.includes('taser') || n.includes('shock') || n.includes('laser')) + return `${W}/pistol.png`; + if (n.includes('crossbow')) + return `${W}/pistol.png`; + // Melee — blunt + if (n.includes('pipe') || n.includes('bat') || n.includes('baton') || n.includes('chain') || n.includes('crowbar') || n.includes('knuckle') || n.includes('wrench') || n.includes('hammer')) + return `${W}/baton.png`; + // Melee — cutting + if (n.includes('chainsaw') || n.includes('saw')) + return `${W}/chainsaw.png`; + if (n.includes('hatchet') || n.includes('axe') || n.includes('cleaver')) + return `${W}/hatchet.png`; + if (n.includes('machete') || n.includes('sabre') || n.includes('sword') || n.includes('blade') || n.includes('vibro')) + return `${W}/sabre.png`; + if (n.includes('wire') || n.includes('stiletto') || n.includes('scalpel') || n.includes('bone saw')) + return `${W}/combat-knife.png`; + if (n.includes('knife') || n.includes('dagger')) + return `${W}/knife.png`; + if (n.includes('fist') || n.includes('punch') || n.includes('slam')) + return `${W}/natural-weapon.png`; + // Fallback by range + if (r && r !== 'engaged') return `${W}/pistol.png`; + return `${W}/knife.png`; + } + + /** Choose ability/specialty icon — SLA trait art where possible */ + _getAbilityIcon(text) { + const t = (text || '').toLowerCase(); + const A = SLAGroupNPCTool._SLA + '/abilities'; + if (t.includes('stealth') || t.includes('camouflage') || t.includes('hidden') || t.includes('ambush')) + return 'icons/svg/cowled.svg'; + if (t.includes('acid') || t.includes('contagion') || t.includes('venom') || t.includes('poison') || t.includes('dissolve')) + return `${A}/contagion.png`; + if (t.includes('fire') || t.includes('burn') || t.includes('flame')) + return 'icons/svg/fire.svg'; + if (t.includes('stun') || t.includes('emp') || t.includes('shock') || t.includes('paralys')) + return 'icons/svg/daze.svg'; + if (t.includes('bleed') || t.includes('rend') || t.includes('gore') || t.includes('bite')) + return 'icons/svg/blood.svg'; + if (t.includes('heal') || t.includes('repair') || t.includes('recover') || t.includes('diagnos')) + return `${A}/regeneration.png`; + if (t.includes('pack') || t.includes('swarm') || t.includes('gang') || t.includes('mob') || t.includes('flank')) + return `${A}/enemy.png`; + if (t.includes('frenzy') || t.includes('unstoppable') || t.includes('brutal') || t.includes('hard hit')) + return `${A}/hard-hitting.png`; + if (t.includes('fast') || t.includes('speed') || t.includes('sprint') || t.includes('leap') || t.includes('pounce')) + return `${A}/fast-reflexes.png`; + if (t.includes('intimidat') || t.includes('menac') || t.includes('fear') || t.includes('screech')) + return `${A}/menacing.png`; + if (t.includes('psycho') || t.includes('endurance') || t.includes('mindless') || t.includes('ignore')) + return `${A}/psychosis.png`; + return `${A}/attack.png`; + } + + /** Find or create an Actor folder by name, optionally under a parent */ + async _ensureFolder(name, parentId = null) { + let folder = (game.folders ?? []).find(f => + f.type === 'Actor' && f.name === name && (f.folder?.id ?? null) === parentId + ); + if (!folder) { + try { + const colors = { + 'Generated NPCs': '#442211', + 'Street Gang': '#996611', + 'Carrion': '#883311', + 'Serial Killer': '#661133', + 'Monstaret': '#224411', + 'Machine': '#112244', + }; + folder = await Folder.create({ + name, type: 'Actor', + folder: parentId, + sorting: 'a', + color: colors[name] ?? '#333333' + }); + } catch(e) { + console.warn(`SLA Zero Two | Could not create folder "${name}":`, e); + } + } + return folder ?? null; + } + + // ── Save in-progress edits before re-render ─────────────────────────────── + _saveState() { + const root = this._root; + if (!root) return; + root.querySelectorAll('.gnpc-name-input').forEach(el => { + const n = this._npcs.find(x => x.id === el.dataset.npcId); + if (n) n.name = el.value; + }); + root.querySelectorAll('.gnpc-stat-input').forEach(el => { + const n = this._npcs.find(x => x.id === el.dataset.npcId); + if (n && el.dataset.field) n[el.dataset.field] = parseInt(el.value) || 0; + }); + root.querySelectorAll('.gnpc-notes-input').forEach(el => { + const n = this._npcs.find(x => x.id === el.dataset.npcId); + if (n) n.notes = el.value; + }); + } + + // ── Build one NPC card ──────────────────────────────────────────────────── + _buildCard(npc) { + const rgb = this._hexRgb(npc.color); + const hpPct = npc.hpMax > 0 ? Math.max(0, Math.min(100, (npc.hp / npc.hpMax) * 100)) : 0; + const hpColor = hpPct > 60 ? '#44cc66' : hpPct > 30 ? '#ffaa00' : '#cc3333'; + const defeated = npc.defeated ? ' gnpc-defeated' : ''; + const bgStyle = `border-left:3px solid ${npc.color};background:rgba(${rgb},0.07)`; + + // Natural weapons (creatures only) + const natHtml = (npc.naturalWeapons ?? []).map(w => { + const detail = `DMG ${w.damage}${w.ap ? ` AP ${w.ap}` : ''}${w.note ? ` — ${w.note}` : ''}`; + return `
${this._esc(w.name)}${detail}
`; + }).join(''); + + // Conventional / integrated weapons + const wepHtml = (npc.weapons ?? []).map(w => { + const detail = `DMG ${w.damage}${w.ap ? ` AP ${w.ap}` : ''}${w.range ? ` [${w.range}]` : ''}${w.note ? ` — ${w.note}` : ''}`; + return `
${this._esc(w.name)}${detail}
`; + }).join(''); + + // Abilities + const abils = (npc.abilities ?? []).map(a => + `
${this._esc(a)}
` + ).join(''); + + const armorLine = npc.armorDesc && npc.armorDesc !== 'None' + ? `
${this._esc(npc.armorDesc)}
` : ''; + + return ` +
+
+ + + ${npc.typeLabel} + + T${npc.threat} + ${(npc.threatBonus ?? 1) > 1 ? `⚡${npc.threatBonus}` : ''} +
+ + + +
+
+
+ + + + + | +
+ HP + + + / + + +
+
+ | + + +
+
+ + ${npc.initPool}d6 + +
+ ${armorLine} +
${natHtml}${wepHtml}
+
${abils}
+ +
`; + } + + // ── Full render ─────────────────────────────────────────────────────────── + async _renderInner(_data) { + const types = Object.keys(SLA_NPC_GROUP_ARCHETYPES); + + const typeBtns = types.map(k => { + const a = SLA_NPC_GROUP_ARCHETYPES[k]; + const act = k === this._selectedType; + const rgb = this._hexRgb(a.color); + return ``; + }).join(''); + + const countVal = this._count; + + const alive = this._npcs.filter(n => !n.defeated).length; + const defeated = this._npcs.filter(n => n.defeated).length; + const cards = this._npcs.map(n => this._buildCard(n)).join(''); + const empty = `
Choose a type and press Generate to create NPCs.
`; + + const footer = this._npcs.length ? ` + ` : ''; + + return $(` +
+
+
${typeBtns}
+
+ + + + + +
+
+
${this._npcs.length ? cards : empty}
+ ${footer} +
`); + } + + // ── Listeners ───────────────────────────────────────────────────────────── + activateListeners(html) { + super.activateListeners(html); + const root = html instanceof jQuery ? html[0] : html; + this._root = root; + + // Type tab selection + root.querySelectorAll('.gnpc-type-btn').forEach(btn => + btn.addEventListener('click', () => { + this._saveState(); + this._selectedType = btn.dataset.type; + this.render(false); + }) + ); + + // Count selector (number input, 1–10) + root.querySelector('.gnpc-count-sel')?.addEventListener('input', e => { + this._count = Math.max(1, Math.min(10, parseInt(e.target.value) || 1)); + }); + + // Threat level slider (1–10) — live label + track-fill update, no re-render + const threatSlider = root.querySelector('.gnpc-threat-slider'); + const _updateSliderFill = (el, lvl) => { + const pct = ((lvl - 1) / 9 * 100).toFixed(1); + el.style.background = `linear-gradient(to right, #00ccff 0%, #00ccff ${pct}%, #333 ${pct}%, #333 100%)`; + }; + if (threatSlider) { + _updateSliderFill(threatSlider, this._threatLevel); + threatSlider.addEventListener('input', e => { + this._threatLevel = Math.max(1, Math.min(10, parseInt(e.target.value) || 1)); + _updateSliderFill(e.target, this._threatLevel); + const valEl = root.querySelector('.gnpc-threat-val'); + if (valEl) valEl.textContent = this._threatLevelLabel(this._threatLevel); + }); + } + + // Generate (replace all) + root.querySelector('.gnpc-gen-btn')?.addEventListener('click', () => { + this._npcs = []; + for (let i = 0; i < this._count; i++) { + const n = this._generateOne(this._selectedType); + if (n) this._npcs.push(n); + } + this.render(false); + }); + + // Add more (append) + root.querySelector('.gnpc-add-btn')?.addEventListener('click', () => { + this._saveState(); + for (let i = 0; i < this._count; i++) { + const n = this._generateOne(this._selectedType); + if (n) this._npcs.push(n); + } + this.render(false); + }); + + // Clear all + root.querySelector('.gnpc-clr-btn')?.addEventListener('click', () => { + this._npcs = []; + this.render(false); + }); + + // Defeat toggle + root.querySelectorAll('.gnpc-defeat-btn').forEach(btn => + btn.addEventListener('click', () => { + this._saveState(); + const n = this._npcs.find(x => x.id === btn.dataset.npcId); + if (n) { n.defeated = !n.defeated; this.render(false); } + }) + ); + + // Initiative roll — pool = max(5, agi) + threatLevel; count 6s as successes + root.querySelectorAll('.gnpc-init-btn').forEach(btn => + btn.addEventListener('click', () => { + const n = this._npcs.find(x => x.id === btn.dataset.npcId); + if (!n) return; + const pool = n.initPool ?? Math.max(5, n.agi) + (n.threatBonus ?? 1); + const rolls = Array.from({ length: pool }, () => Math.ceil(Math.random() * 6)); + const successes = rolls.filter(d => d === 6).length; + + // Update inline result display (no full re-render) + const resultEl = root.querySelector(`.gnpc-init-result[data-npc-id="${n.id}"]`); + if (resultEl) { + const diceHtml = rolls.map(d => + `${d}` + ).join(''); + resultEl.innerHTML = `${diceHtml}${successes}✓`; + } + + // Post result to chat + const diceStr = rolls.map(d => d === 6 ? `${d}` : `${d}`).join(' '); + ChatMessage.create({ + speaker: { alias: n.name }, + content: `
+ ${n.name} — Initiative (${pool}d6)
+ ${diceStr}
+ ${successes} success${successes !== 1 ? 'es' : ''} +
`, + type: CONST.CHAT_MESSAGE_TYPES?.OTHER ?? 0, + }); + }) + ); + + // Reroll one + root.querySelectorAll('.gnpc-reroll-btn').forEach(btn => + btn.addEventListener('click', () => { + this._saveState(); + const idx = this._npcs.findIndex(x => x.id === btn.dataset.npcId); + if (idx < 0) return; + const fresh = this._generateOne(this._npcs[idx].typeKey); + if (fresh) { fresh.notes = this._npcs[idx].notes; this._npcs[idx] = fresh; } + this.render(false); + }) + ); + + // Remove one + root.querySelectorAll('.gnpc-remove-btn').forEach(btn => + btn.addEventListener('click', () => { + this._saveState(); + this._npcs = this._npcs.filter(x => x.id !== btn.dataset.npcId); + this.render(false); + }) + ); + + // HP ± buttons (no full re-render — update DOM directly) + root.querySelectorAll('.gnpc-hp-down').forEach(btn => + btn.addEventListener('click', () => { + const n = this._npcs.find(x => x.id === btn.dataset.npcId); + if (!n) return; + n.hp = Math.max(0, n.hp - 1); + const card = root.querySelector(`.gnpc-card[data-npc-id="${n.id}"]`); + if (card) { + card.querySelector('.gnpc-hp-val').value = n.hp; + const pct = n.hpMax > 0 ? Math.max(0, (n.hp / n.hpMax) * 100) : 0; + const color = pct > 60 ? '#44cc66' : pct > 30 ? '#ffaa00' : '#cc3333'; + const bar = card.querySelector('.gnpc-hp-bar'); + if (bar) { bar.style.width = `${pct}%`; bar.style.background = color; } + } + }) + ); + root.querySelectorAll('.gnpc-hp-up').forEach(btn => + btn.addEventListener('click', () => { + const n = this._npcs.find(x => x.id === btn.dataset.npcId); + if (!n) return; + n.hp = Math.min(n.hpMax, n.hp + 1); + const card = root.querySelector(`.gnpc-card[data-npc-id="${n.id}"]`); + if (card) { + card.querySelector('.gnpc-hp-val').value = n.hp; + const pct = n.hpMax > 0 ? Math.max(0, (n.hp / n.hpMax) * 100) : 0; + const color = pct > 60 ? '#44cc66' : pct > 30 ? '#ffaa00' : '#cc3333'; + const bar = card.querySelector('.gnpc-hp-bar'); + if (bar) { bar.style.width = `${pct}%`; bar.style.background = color; } + } + }) + ); + + // Reset all HP to max + root.querySelector('.gnpc-reset-hp-btn')?.addEventListener('click', () => { + this._saveState(); + this._npcs.forEach(n => { n.hp = n.hpMax; n.defeated = false; }); + this.render(false); + }); + + // Inline stat edits — save to state on input (no re-render) + root.querySelectorAll('.gnpc-stat-input').forEach(el => + el.addEventListener('input', () => { + const n = this._npcs.find(x => x.id === el.dataset.npcId); + if (n && el.dataset.field) n[el.dataset.field] = parseInt(el.value) || 0; + }) + ); + root.querySelectorAll('.gnpc-name-input').forEach(el => + el.addEventListener('input', () => { + const n = this._npcs.find(x => x.id === el.dataset.npcId); + if (n) n.name = el.value; + }) + ); + root.querySelectorAll('.gnpc-notes-input').forEach(el => + el.addEventListener('input', () => { + const n = this._npcs.find(x => x.id === el.dataset.npcId); + if (n) n.notes = el.value; + }) + ); + + // Create as World Actors — folder + icons + embedded items + root.querySelector('.gnpc-create-btn')?.addEventListener('click', async () => { + if (!game.user?.isGM) return; + this._saveState(); + + // Get/create folder hierarchy: Generated NPCs > Type + const rootFolder = await this._ensureFolder('Generated NPCs'); + const typeLabel = SLA_NPC_GROUP_ARCHETYPES[this._selectedType]?.label ?? 'NPCs'; + const typeFolder = await this._ensureFolder(typeLabel, rootFolder?.id ?? null); + const folderId = typeFolder?.id ?? rootFolder?.id ?? null; + + let created = 0; + for (const npc of this._npcs) { + try { + // Biography + const bio = [`Type: ${npc.typeLabel} | Threat: ${npc.threat}`]; + if (npc.armorDesc && npc.armorDesc !== 'None') bio.push(`Armour: ${npc.armorDesc}`); + const allWep = [...(npc.naturalWeapons ?? []), ...(npc.weapons ?? [])]; + if (allWep.length) { + bio.push('\nWeapons:'); + allWep.forEach(w => bio.push(` ${w.name} — DMG ${w.damage}${w.ap ? ` AP ${w.ap}` : ''}${w.range ? ` [${w.range}]` : ''}${w.note ? ` — ${w.note}` : ''}`)); + } + if (npc.abilities?.length) { + bio.push('\nAbilities:'); + npc.abilities.forEach(a => bio.push(` ${a}`)); + } + if (npc.notes) bio.push(`\nNotes:\n ${npc.notes}`); + + // Create actor with portrait icon + const actorDoc = await Actor.create({ + name: npc.name, type: 'npc', + img: this._getNPCIcon(npc.typeKey), + folder: folderId, + system: { + biography: bio.join('\n'), + threat: npc.threat, + attributes: { strength: npc.str, agility: npc.agi, wits: npc.wit, empathy: npc.emp }, + health: { value: npc.hp, max: npc.hpMax }, + armor: npc.armor, + damage: npc.damage + } + }); + if (!actorDoc) continue; + + // Build embedded items + const embeds = []; + + // Weapon items + for (const w of allWep) { + const isRanged = w.range && w.range !== 'Engaged'; + const rofMatch = w.note?.match(/ROF\s*(\d+)/i); + embeds.push({ + name: w.name, type: 'weapon', + img: this._getWeaponIcon(w), + system: { + description: w.note || '', weaponType: w.name, + category: isRanged ? 'ranged' : 'melee', + damage: w.damage, range: w.range || 'Engaged', ap: w.ap || 0, + gearBonus: 0, rof: rofMatch ? parseInt(rofMatch[1]) : 1, + magazine: isRanged ? 12 : 0, ammo: isRanged ? 12 : 0, + fireModes: isRanged ? ['single'] : [], equipped: true, + ammoType: 'standard', autoAmmoUse: 8, initiativeMod: 0, ammoEmpty: false + } + }); + } + + // Armor item + if (npc.armor > 0 && npc.armorDesc && npc.armorDesc !== 'None' && !npc.armorDesc.startsWith('Built-in')) { + const armorName = npc.armorDesc.replace(/\s*\(ARM \d+\)/, '').trim() || 'Armour'; + const ART = SLAGroupNPCTool._SLA + '/armor'; + const armorImg = npc.armor >= 5 ? `${ART}/exo.png` + : npc.armor >= 3 ? `${ART}/heavy.png` + : npc.armorDesc.toLowerCase().includes('street') ? `${ART}/street.png` + : `${ART}/light.png`; + embeds.push({ + name: armorName, type: 'armor', + img: armorImg, + system: { + description: npc.armorDesc, armorDice: npc.armor, armorAuto: 0, + armorRating: npc.armor, statMod: 0, skillMod: 0, statModTarget: '', + skillModTarget: '', healthMod: 0, resolveMod: 0, initiativeMod: 0, equipped: true + } + }); + } + + // Specialty items for abilities + for (const ability of (npc.abilities ?? [])) { + const title = ability.match(/^([^—\-]+)/)?.[1]?.trim() ?? ability.slice(0, 30); + embeds.push({ + name: title, type: 'specialty', + img: this._getAbilityIcon(ability), + system: { + description: ability, effects: ability, category: 'general', + package: npc.typeLabel, prerequisites: '', isActive: true, + healthMod: 0, resolveMod: 0 + } + }); + } + + if (embeds.length) { + try { await actorDoc.createEmbeddedDocuments('Item', embeds); } + catch(e) { console.warn(`SLA Zero Two | Group NPC: embed failed for "${npc.name}"`, e); } + } + created++; + } catch(err) { + console.error(`SLA Zero Two | Group NPC: failed to create "${npc.name}"`, err); + } + } + ui.notifications.info(`Created ${created} NPC${created !== 1 ? 's' : ''} in folder: Generated NPCs / ${typeLabel}`); + }); + } +} + +// ── GM PC STATUS WINDOW ─────────────────────────────────────────────────────── +/** + * SLAGMStatusWindow — a draggable, auto-refreshing window showing all PC + * conditions at a glance. Open via the scene-controls clipboard button. + * The GM can also click "Post to Chat" to whisper the same report. + */ +class SLAGMStatusWindow extends Application { + constructor(...args) { + super(...args); + this._hooks = []; + } + + static get defaultOptions() { + return foundry.utils.mergeObject(super.defaultOptions, { + id: 'sla-gm-status-window', + title: '⚡ PC Status Board', + width: 540, + height: 'auto', + resizable: true, + classes: ['slazero2', 'gm-status-window'] + }); + } + + /** Singleton open / focus */ + static open() { + if (!SLAGMStatusWindow._inst) SLAGMStatusWindow._inst = new SLAGMStatusWindow(); + SLAGMStatusWindow._inst.render(true); + return SLAGMStatusWindow._inst; + } + + _pcActors() { + return (game.actors ?? []).filter(a => + a.type === 'character' && a.system?.details?.isPlayerCharacter + ); + } + + _activeConditions(actor) { + const statuses = actor.statuses ?? new Set(); + return SLA_CONDITIONS.filter(c => statuses.has(c.id)); + } + + _hpColor(cur, max) { + const pct = max > 0 ? cur / max : 1; + if (pct > 0.5) return '#44cc66'; + if (pct > 0.25) return '#ffaa00'; + return '#cc2222'; + } + + async _renderInner() { + const pcs = this._pcActors(); + const rows = pcs.map(actor => { + const sys = actor.system; + const hp = sys.derivedStats?.health ?? { value: 0, max: 0 }; + const res = sys.derivedStats?.resolve ?? { value: 0, max: 0 }; + const stress = sys.derivedStats?.stress ?? 0; + const showOff = sys.details?.showingOff ?? 0; + const conds = this._activeConditions(actor); + const img = actor.img ?? 'icons/svg/mystery-man.svg'; + + const condTags = conds.length ? conds.map(c => { + const allMod = c.diceModifiers?.all ?? 0; + const penalty = allMod ? ` (${allMod > 0 ? '+' : ''}${allMod} all)` : ''; + return ` + ${c.label}${penalty} + + `; + }).join('') : '— clean —'; + + const stressColor = stress === 0 ? '#44cc66' : stress <= 2 ? '#ffaa00' : '#cc2222'; + const soColor = showOff >= 20 ? '#ff4444' : showOff >= 10 ? '#ff9900' : showOff > 0 ? '#ffcc00' : '#888'; + + return `
+ +
+
${actor.name}
+
+ + ❤ ${hp.value}/${hp.max} + + + 🔵 ${res.value}/${res.max} + + + ⚡ ${stress} + + ${showOff > 0 ? `✨ ${showOff}pts` : ''} +
+
${condTags}
+
+
`; + }).join(''); + + const noPC = pcs.length === 0 + ? '
No player characters found.
' + : ''; + + const html = `
+
+ Auto-refreshes on actor changes + +
+
${noPC}${rows}
+
`; + + const el = document.createElement('div'); + el.innerHTML = html; + return $(el); + } + + async _render(force, options) { + await super._render(force, options); + // Register auto-refresh hooks once + if (!this._hooks.length) { + const refresh = () => { if (this.rendered) this.render(false); }; + this._hooks.push( + Hooks.on('updateActor', refresh), + Hooks.on('createActiveEffect', refresh), + Hooks.on('deleteActiveEffect', refresh), + Hooks.on('updateActiveEffect', refresh) + ); + } + } + + async close(options) { + this._hooks.forEach((id, i) => { + const evts = ['updateActor','createActiveEffect','deleteActiveEffect','updateActiveEffect']; + Hooks.off(evts[i], id); + }); + this._hooks = []; + SLAGMStatusWindow._inst = null; + + return super.close(options); + } + + activateListeners(html) { + super.activateListeners(html); + // Open sheet on portrait or name click + html[0].querySelectorAll('.gsw-portrait, .gsw-actor-name').forEach(el => { + el.addEventListener('click', () => { + game.actors.get(el.dataset.actorId)?.sheet?.render(true); + }); + }); + // Remove condition + html[0].querySelectorAll('.gsw-remove-cond').forEach(btn => { + btn.addEventListener('click', async (ev) => { + ev.stopPropagation(); + const actor = game.actors.get(btn.dataset.actorId); + if (!actor) return; + const condId = btn.dataset.condId; + // Remove matching active effects + const toDelete = actor.effects.filter(e => + e.statuses?.has(condId) || e.flags?.core?.statusId === condId + ).map(e => e.id); + if (toDelete.length) await actor.deleteEmbeddedDocuments('ActiveEffect', toDelete); + else await actor.toggleStatusEffect(condId, { active: false }); + }); + }); + // Post to chat + html[0].querySelector('.gsw-post-chat')?.addEventListener('click', () => _broadcastPCConditions()); + } +} +SLAGMStatusWindow._inst = null; + +// ── 💳 CREDIT DISTRIBUTION ──────────────────────────────────────────────────────────── +class SLACreditDistributionTool extends Application { + constructor(...args) { + super(...args); + this._hooks = []; + } + + static get defaultOptions() { + return foundry.utils.mergeObject(super.defaultOptions, { + id: 'sla-credit-tool', + title: '💳 Credit Distribution', + width: 560, + height: 'auto', + resizable: true, + classes: ['slazero2', 'credit-tool-window'] + }); + } + + static open() { + if (!SLACreditDistributionTool._inst) SLACreditDistributionTool._inst = new SLACreditDistributionTool(); + SLACreditDistributionTool._inst.render(true); + return SLACreditDistributionTool._inst; + } + + _pcActors() { + return (game.actors ?? []).filter(a => a.type === 'character' && a.system?.details?.isPlayerCharacter); + } + + _calcIncome(actor) { + const fin = actor.system?.finances?.income ?? {}; + return (fin.salary ?? 0) + (fin.bpnReward ?? 0) + (fin.other ?? 0); + } + + _calcExpenses(actor) { + const exp = actor.system?.finances?.expenses ?? {}; + return (exp.accommodation ?? 0) + (exp.drugs ?? 0) + (exp.subscriptions ?? 0) + (exp.other ?? 0) + (exp.bulletTax ?? 0); + } + + async _renderInner() { + const pcs = this._pcActors(); + + // Build rows for each PC + const rows = pcs.map(actor => { + const credits = actor.system?.details?.credits ?? 0; + const scl = actor.system?.details?.scl ?? 0; + const income = this._calcIncome(actor); + const expenses = this._calcExpenses(actor); + const debt = actor.system?.finances?.debt ?? 0; + const net = income - expenses; + + const netClass = net >= 0 ? 'ctw-positive' : 'ctw-negative'; + const debtClass = debt > 0 ? 'ctw-debt' : ''; + const imgSrc = actor.img ?? 'icons/svg/mystery-man.svg'; + + return ` + + + ${actor.name} + ${actor.name} + SCL ${scl} + + + + + ${income} + ${expenses} + ${debt} + ${net} + + + + `; + }).join(''); + + const emptyMsg = pcs.length === 0 + ? `No player characters found. Mark actors as Player Characters on their sheets.` + : ''; + + const html = ` +
+ +
+

Player Credit Ledger

+ + + + + + + + + + + + + + ${rows} + ${emptyMsg} + +
CharacterCredits ¢IncomeExpensesDebtNet
+
+ +
+

Bulk Distribution

+
+ + +
+ + + +
+
+ + +
+
+
+ +
`; + + return $(html); + } + + async _render(force, options) { + await super._render(force, options); + if (!this._hooks.length) { + const refresh = () => { if (this.rendered) this.render(false); }; + this._hooks.push( + Hooks.on('updateActor', refresh) + ); + } + } + + async close(options) { + this._hooks.forEach((id) => { + Hooks.off('updateActor', id); + }); + this._hooks = []; + SLACreditDistributionTool._inst = null; + return super.close(options); + } + + activateListeners(html) { + super.activateListeners(html); + + // --- Save individual credits --- + html.find('.ctw-btn-save').on('click', async (ev) => { + const actorId = ev.currentTarget.dataset.actorId; + const actor = game.actors.get(actorId); + if (!actor) return ui.notifications.error(`Actor not found: ${actorId}`); + + const input = html.find(`#credits-input-${actorId}`); + const newValue = parseInt(input.val(), 10); + + if (isNaN(newValue) || newValue < 0) { + return ui.notifications.error(`Invalid credit value for ${actor.name}.`); + } + + try { + await actor.update({ 'system.details.credits': newValue }); + ui.notifications.info(`${actor.name}: credits set to ¢${newValue}.`); + } catch (err) { + console.error('SLACreditDistributionTool | save error', err); + ui.notifications.error(`Failed to update credits for ${actor.name}.`); + } + }); + + // --- Pay All PCs --- + html.find('#ctw-pay-all-btn').on('click', async () => { + const pcs = this._pcActors(); + if (pcs.length === 0) return ui.notifications.error('No player characters found.'); + + const amount = parseInt(html.find('#ctw-bulk-amount').val(), 10); + if (isNaN(amount) || amount <= 0) { + return ui.notifications.error('Enter a valid amount greater than 0.'); + } + + const payType = html.find('input[name="ctw-pay-type"]:checked').val(); + + let successCount = 0; + for (const actor of pcs) { + try { + if (payType === 'custom') { + // Custom: add directly to credits balance + const currentCredits = actor.system?.details?.credits ?? 0; + await actor.update({ 'system.details.credits': currentCredits + amount }); + } else if (payType === 'salary') { + await actor.update({ 'system.finances.income.salary': amount }); + } else if (payType === 'bpnReward') { + await actor.update({ 'system.finances.income.bpnReward': amount }); + } + successCount++; + } catch (err) { + console.error(`SLACreditDistributionTool | pay error for ${actor.name}`, err); + ui.notifications.error(`Failed to pay ${actor.name}.`); + } + } + + if (successCount > 0) { + const label = payType === 'custom' ? `¢${amount} credits` : `${payType} set to ¢${amount}`; + ui.notifications.info(`Paid ${successCount} PC(s): ${label}.`); + } + }); + + // --- Deduct All Expenses --- + html.find('#ctw-deduct-expenses-btn').on('click', async () => { + const pcs = this._pcActors(); + if (pcs.length === 0) return ui.notifications.error('No player characters found.'); + + let successCount = 0; + for (const actor of pcs) { + try { + const currentCredits = actor.system?.details?.credits ?? 0; + const totalExpenses = this._calcExpenses(actor); + const newCredits = Math.max(0, currentCredits - totalExpenses); + await actor.update({ 'system.details.credits': newCredits }); + successCount++; + } catch (err) { + console.error(`SLACreditDistributionTool | deduct error for ${actor.name}`, err); + ui.notifications.error(`Failed to deduct expenses for ${actor.name}.`); + } + } + + if (successCount > 0) { + ui.notifications.info(`Expenses deducted from ${successCount} PC(s).`); + } + }); + } +} + +SLACreditDistributionTool._inst = null; + +// ── 📒 SHIFT LEDGER ──────────────────────────────────────────────────────────── +class SLAShiftLedger extends Application { + constructor(...args) { + super(...args); + this._hooks = []; + } + + static get defaultOptions() { + return foundry.utils.mergeObject(super.defaultOptions, { + id: 'sla-shift-ledger', + title: '📒 Shift Ledger', + width: 700, + height: 'auto', + resizable: true, + classes: ['slazero2', 'shift-ledger-window'] + }); + } + + static open() { + if (!SLAShiftLedger._inst) SLAShiftLedger._inst = new SLAShiftLedger(); + SLAShiftLedger._inst.render(true); + return SLAShiftLedger._inst; + } + + _pcActors() { + return (game.actors ?? []).filter(a => a.type === 'character' && a.system?.details?.isPlayerCharacter); + } + + _calcNet(actor) { + const fin = actor.system?.finances ?? {}; + const inc = fin.income ?? {}; + const exp = fin.expenses ?? {}; + const totalIncome = (inc.salary || 0) + (inc.bpnReward || 0) + (inc.other || 0); + const totalExpenses = (exp.accommodation || 0) + (exp.drugs || 0) + (exp.subscriptions || 0) + (exp.other || 0) + (exp.bulletTax || 0); + return { totalIncome, totalExpenses, net: totalIncome - totalExpenses }; + } + + async _processShift(actor) { + const { totalIncome, totalExpenses } = this._calcNet(actor); + const currentCredits = actor.system?.details?.credits ?? 0; + const currentDebt = actor.system?.finances?.debt ?? 0; + + let newCredits = currentCredits + totalIncome - totalExpenses; + let newDebt = currentDebt; + + if (newCredits < 0) { + newDebt = currentDebt + Math.abs(newCredits); + newCredits = 0; + } + + try { + await actor.update({ + 'system.details.credits': newCredits, + 'system.finances.debt': newDebt + }); + ui.notifications.info(`Shift processed for ${actor.name}: ${newCredits}¢ credits, ${newDebt}¢ debt.`); + } catch (err) { + console.error('SLAShiftLedger | Failed to process shift for', actor.name, err); + ui.notifications.error(`Failed to process shift for ${actor.name}. See console for details.`); + } + } + + async _processAllShifts() { + const pcs = this._pcActors(); + if (!pcs.length) { + ui.notifications.error('No player characters found to process.'); + return; + } + for (const actor of pcs) { + await this._processShift(actor); + } + ui.notifications.info(`Shift processed for all ${pcs.length} player character(s).`); + } + + async _exportToChat() { + const pcs = this._pcActors(); + if (!pcs.length) { + ui.notifications.error('No player characters found to export.'); + return; + } + + let lines = ['

📒 Shift Ledger Summary

']; + lines.push(''); + + let totalCredits = 0, totalIncome = 0, totalExpenses = 0, totalDebt = 0; + + for (const actor of pcs) { + const { totalIncome: inc, totalExpenses: exp, net } = this._calcNet(actor); + const credits = actor.system?.details?.credits ?? 0; + const scl = actor.system?.details?.scl ?? '–'; + const debt = actor.system?.finances?.debt ?? 0; + totalCredits += credits; + totalIncome += inc; + totalExpenses += exp; + totalDebt += debt; + const netColor = net >= 0 ? '#44cc66' : '#cc2222'; + const debtColor = debt > 0 ? '#cc2222' : 'inherit'; + lines.push( + `` + + `` + + `` + + `` + + `` + + `` + + `` + + `` + + `` + ); + } + + const groupNet = totalIncome - totalExpenses; + const groupNetColor = groupNet >= 0 ? '#44cc66' : '#cc2222'; + lines.push( + `` + + `` + + `` + + `` + + `` + + `` + + `` + + `` + ); + lines.push('
CharacterSCLCreditsIncomeExpensesNetDebt
${actor.name}${scl}${credits}¢+${inc}¢-${exp}¢${net >= 0 ? '+' : ''}${net}¢${debt}¢
GROUP TOTALS${totalCredits}¢+${totalIncome}¢-${totalExpenses}¢${groupNet >= 0 ? '+' : ''}${groupNet}¢${totalDebt}¢
'); + + try { + await ChatMessage.create({ + content: lines.join(''), + whisper: [game.user.id], + speaker: { alias: 'Shift Ledger' } + }); + ui.notifications.info('Shift ledger exported to chat (GM only).'); + } catch (err) { + console.error('SLAShiftLedger | Failed to export to chat:', err); + ui.notifications.error('Failed to export ledger to chat. See console for details.'); + } + } + + async _renderInner() { + const pcs = this._pcActors(); + + // ── Aggregate totals ────────────────────────────────────────────────────── + let aggCredits = 0, aggIncome = 0, aggExpenses = 0, aggDebt = 0; + for (const actor of pcs) { + const { totalIncome, totalExpenses } = this._calcNet(actor); + aggCredits += actor.system?.details?.credits ?? 0; + aggIncome += totalIncome; + aggExpenses += totalExpenses; + aggDebt += actor.system?.finances?.debt ?? 0; + } + const aggNet = aggIncome - aggExpenses; + + // ── Summary bar ────────────────────────────────────────────────────────── + const summaryBar = ` +
+
+ Group Credits + ${aggCredits}¢ +
+
+ Total Income + +${aggIncome}¢ +
+
+ Total Expenses + -${aggExpenses}¢ +
+
+ Net This Shift + ${aggNet >= 0 ? '+' : ''}${aggNet}¢ +
+
+ Total Debt + ${aggDebt}¢ +
+
`; + + // ── Per-PC cards ───────────────────────────────────────────────────────── + let pcCards = ''; + if (pcs.length === 0) { + pcCards = '
No player characters found. Mark actors as Player Characters in their sheet details.
'; + } else { + for (const actor of pcs) { + const det = actor.system?.details ?? {}; + const fin = actor.system?.finances ?? {}; + const inc = fin.income ?? {}; + const exp = fin.expenses ?? {}; + const { totalIncome, totalExpenses, net } = this._calcNet(actor); + const credits = det.credits ?? 0; + const scl = det.scl ?? '–'; + const debt = fin.debt ?? 0; + + pcCards += ` +
+
+ ${actor.name} + SCL ${scl} + ${credits}¢ +
+ +
+
+
Income
+
+ Salary + ${inc.salary ?? 0}¢ +
+
+ BPN Reward + ${inc.bpnReward ?? 0}¢ +
+
+ Other + ${inc.other ?? 0}¢ +
+
+ Total Income + +${totalIncome}¢ +
+
+ +
+
Expenses
+
+ Accommodation + ${exp.accommodation ?? 0}¢ +
+
+ Drugs + ${exp.drugs ?? 0}¢ +
+
+ Subscriptions + ${exp.subscriptions ?? 0}¢ +
+
+ Other + ${exp.other ?? 0}¢ +
+
+ Bullet Tax + ${exp.bulletTax ?? 0}¢ +
+
+ Total Expenses + -${totalExpenses}¢ +
+
+ +
+
Summary
+
+ Net Balance + ${net >= 0 ? '+' : ''}${net}¢ +
+
+ Debt + ${debt}¢ +
+ +
+
+
`; + } + } + + // ── Global action bar ──────────────────────────────────────────────────── + const actionBar = ` +
+ + +
`; + + const html = ` +
+ ${summaryBar} +
+ ${pcCards} +
+ ${actionBar} +
`; + + return $(html); + } + + async _render(force, options) { + await super._render(force, options); + if (!this._hooks.length) { + const refresh = () => { if (this.rendered) this.render(false); }; + this._hooks.push(Hooks.on('updateActor', refresh)); + } + } + + async close(options) { + this._hooks.forEach(id => Hooks.off('updateActor', id)); + this._hooks = []; + SLAShiftLedger._inst = null; + return super.close(options); + } + + activateListeners(html) { + super.activateListeners(html); + + // Per-PC process shift buttons + html.find('.slw-btn--process').on('click', async (ev) => { + const actorId = ev.currentTarget.dataset.actorId; + const actor = game.actors.get(actorId); + if (!actor) { + ui.notifications.error('Actor not found.'); + return; + } + await this._processShift(actor); + }); + + // Process all shifts button + html.find('#slw-process-all').on('click', async () => { + await this._processAllShifts(); + }); + + // Export to chat button + html.find('#slw-export-chat').on('click', async () => { + await this._exportToChat(); + }); + } +} + +SLAShiftLedger._inst = null; + +// ── ☠ NPC THREAT BOARD ──────────────────────────────────────────────────────────── +// ────────────────────────────────────────────────────────────────────────────── +// SLANPCThreatBoard — GM tactical overview of all NPC actors +// ────────────────────────────────────────────────────────────────────────────── + +function threatLabel(val) { + if (!val || val <= 2) return { label: 'Low', cls: 'threat-low' }; + if (val <= 3) return { label: 'Med', cls: 'threat-med' }; + if (val <= 5) return { label: 'High', cls: 'threat-high' }; + return { label: 'Elite', cls: 'threat-elite' }; +} + +function hpColor(cur, max) { + const pct = max > 0 ? cur / max : 1; + if (pct > 0.50) return '#44cc66'; + if (pct > 0.25) return '#ffaa00'; + return '#cc2222'; +} + +class SLANPCThreatBoard extends Application { + constructor(...args) { + super(...args); + this._hooks = []; + this._filterThreat = 'all'; // 'all' | 'low' | 'med' | 'high' | 'elite' + this._filterScene = false; // show only actors present in the active scene + this._showDead = false; // include dead actors + this._sortBy = 'threat'; // 'name' | 'threat' | 'health' + } + + // ── AppV1 config ───────────────────────────────────────────────────────────── + + static get defaultOptions() { + return foundry.utils.mergeObject(super.defaultOptions, { + id: 'sla-npc-threat-board', + title: '☠ NPC Threat Board', + width: 640, + height: 500, + resizable: true, + classes: ['slazero2', 'npc-threat-board'] + }); + } + + static open() { + if (!SLANPCThreatBoard._inst) SLANPCThreatBoard._inst = new SLANPCThreatBoard(); + SLANPCThreatBoard._inst.render(true); + return SLANPCThreatBoard._inst; + } + + // ── Data helpers ────────────────────────────────────────────────────────────── + + _npcActors() { + return (game.actors ?? []).filter( + a => a.type === 'npc' || (a.type === 'character' && !a.system?.details?.isPlayerCharacter) + ); + } + + /** Returns a Set of actorIds present in the currently active scene. */ + _sceneActorIds() { + const ids = new Set(); + const tokens = game.scenes?.active?.tokens ?? []; + for (const t of tokens) { + if (t.actorId) ids.add(t.actorId); + } + return ids; + } + + // ── HTML builder ────────────────────────────────────────────────────────────── + + async _renderInner() { + const sceneIds = this._sceneActorIds(); + let actors = this._npcActors(); + + // ── Filter: scene ──────────────────────────────────────────────────────── + if (this._filterScene) { + actors = actors.filter(a => sceneIds.has(a.id)); + } + + // ── Filter: dead ───────────────────────────────────────────────────────── + if (!this._showDead) { + actors = actors.filter(a => { + const dead = a.statuses?.has('dead') ?? false; + const hpVal = a.system?.health?.value ?? 0; + return !dead && hpVal > 0; + }); + } + + // ── Filter: threat tier ────────────────────────────────────────────────── + if (this._filterThreat !== 'all') { + actors = actors.filter(a => { + const t = a.system?.threat || 0; + const { cls } = threatLabel(t); + return cls === `threat-${this._filterThreat}`; + }); + } + + // ── Sort ───────────────────────────────────────────────────────────────── + if (this._sortBy === 'name') { + actors.sort((a, b) => (a.name ?? '').localeCompare(b.name ?? '')); + } else if (this._sortBy === 'threat') { + actors.sort((a, b) => (b.system?.threat || 0) - (a.system?.threat || 0)); + } else if (this._sortBy === 'health') { + actors.sort((a, b) => { + const pctA = (a.system?.health?.max || 0) > 0 + ? (a.system.health.value || 0) / a.system.health.max : 0; + const pctB = (b.system?.health?.max || 0) > 0 + ? (b.system.health.value || 0) / b.system.health.max : 0; + return pctA - pctB; // lowest HP first — most urgent + }); + } + + // ── Build card HTML ────────────────────────────────────────────────────── + const cards = actors.map(actor => { + const hpCur = actor.system?.health?.value ?? 0; + const hpMax = actor.system?.health?.max ?? 0; + const threat = actor.system?.threat ?? 0; + const str = actor.system?.attributes?.strength ?? 0; + const agi = actor.system?.attributes?.agility ?? 0; + const wit = actor.system?.attributes?.wits ?? 0; + const emp = actor.system?.attributes?.empathy ?? 0; + const dmg = actor.system?.damage ?? '—'; + const armor = actor.system?.armor ?? 0; + const imgSrc = actor.img ?? 'icons/svg/mystery-man.svg'; + + const { label: tLabel, cls: tCls } = threatLabel(threat); + const hpPct = hpMax > 0 ? Math.max(0, Math.min(1, hpCur / hpMax)) : 0; + const hpBarPct = Math.round(hpPct * 100); + const barColor = hpColor(hpCur, hpMax); + + const isDead = (actor.statuses?.has('dead') ?? false) || hpCur <= 0; + + // Status dots (all active statuses other than 'dead') + const statuses = actor.statuses ? [...actor.statuses].filter(s => s !== 'dead') : []; + const statusDots = statuses.slice(0, 6).map(s => + `` + ).join(''); + + // Scene indicator + const inScene = sceneIds.has(actor.id) + ? `◉` + : ''; + + return ` +
+ ${isDead ? '
💀
' : ''} + +
+
+ ${actor.name} +
+
+
${actor.name}${inScene}
+ ${tLabel} ${threat || '–'} +
+
+ +
+
+
+
+
+ + ${hpCur} / ${hpMax} + +
+
+ +
+
STR${str}
+
AGI${agi}
+
WIT${wit}
+
EMP${emp}
+
+ +
+ ⚔ ${dmg} + 🛡 ${armor} +
${statusDots}
+
+
`; + }).join(''); + + const emptyMsg = actors.length === 0 + ? `
No NPCs match the current filters.
` + : ''; + + // ── Filter / sort bar HTML ──────────────────────────────────────────────── + const html = ` +
+ +
+ +
+ + + +
+ +
+ + + + + +
+ +
+ Sort: + + + +
+ +
+ +
+ ${cards} + ${emptyMsg} +
+ +
`; + + return $(html); + } + + // ── Lifecycle ───────────────────────────────────────────────────────────────── + + async _render(force, options) { + await super._render(force, options); + if (!this._hooks.length) { + const refresh = () => { if (this.rendered) this.render(false); }; + this._hooks.push( + Hooks.on('updateActor', refresh), + Hooks.on('createActor', refresh), + Hooks.on('deleteActor', refresh) + ); + } + } + + async close(options) { + const evts = ['updateActor', 'createActor', 'deleteActor']; + this._hooks.forEach((id, i) => Hooks.off(evts[i], id)); + this._hooks = []; + SLANPCThreatBoard._inst = null; + return super.close(options); + } + + // ── Listeners ───────────────────────────────────────────────────────────────── + + activateListeners(html) { + super.activateListeners(html); + + // ── Scene filter buttons ───────────────────────────────────────────────── + html.find('[data-filter-scene]').on('click', (ev) => { + this._filterScene = ev.currentTarget.dataset.filterScene === 'true'; + this.render(false); + }); + + // ── Dead toggle ────────────────────────────────────────────────────────── + html.find('.ntb-toggle-dead').on('click', () => { + this._showDead = !this._showDead; + this.render(false); + }); + + // ── Threat filter buttons ──────────────────────────────────────────────── + html.find('.ntb-threat-filter').on('click', (ev) => { + this._filterThreat = ev.currentTarget.dataset.threat || 'all'; + this.render(false); + }); + + // ── Sort buttons ───────────────────────────────────────────────────────── + html.find('.ntb-sort-btn').on('click', (ev) => { + this._sortBy = ev.currentTarget.dataset.sort || 'threat'; + this.render(false); + }); + + // ── HP decrement ───────────────────────────────────────────────────────── + html.find('.ntb-hp-dec').on('click', async (ev) => { + ev.stopPropagation(); + const actorId = ev.currentTarget.dataset.actorId; + const actor = game.actors?.get(actorId); + if (!actor) return; + const cur = actor.system?.health?.value ?? 0; + const newVal = cur - 1; + try { + await actor.update({ 'system.health.value': newVal }); + } catch (err) { + console.error('SLANPCThreatBoard | HP decrement error', err); + ui.notifications?.error(`Failed to update HP for ${actor.name}.`); + } + }); + + // ── HP increment ───────────────────────────────────────────────────────── + html.find('.ntb-hp-inc').on('click', async (ev) => { + ev.stopPropagation(); + const actorId = ev.currentTarget.dataset.actorId; + const actor = game.actors?.get(actorId); + if (!actor) return; + const cur = actor.system?.health?.value ?? 0; + const hpMax = actor.system?.health?.max ?? 0; + const newVal = hpMax > 0 ? Math.min(hpMax, cur + 1) : cur + 1; + try { + await actor.update({ 'system.health.value': newVal }); + } catch (err) { + console.error('SLANPCThreatBoard | HP increment error', err); + ui.notifications?.error(`Failed to update HP for ${actor.name}.`); + } + }); + + // ── Open actor sheet on card click ─────────────────────────────────────── + html.find('.ntb-card').on('click', (ev) => { + // Don't open sheet if a HP button was clicked + if (ev.target.closest('.ntb-hp-btn')) return; + const actorId = ev.currentTarget.dataset.actorId; + const actor = game.actors?.get(actorId); + if (actor) actor.sheet.render(true); + }); + } +} + +SLANPCThreatBoard._inst = null; + +// ── 📋 BPN TRACKER ──────────────────────────────────────────────────────────── +class SLABPNTracker extends Application { + constructor(...args) { + super(...args); + this._hooks = []; + this._editMode = false; + } + + static get defaultOptions() { + return foundry.utils.mergeObject(super.defaultOptions, { + id: 'sla-bpn-tracker', + title: '📋 BPN Tracker', + width: 520, + height: 'auto', + resizable: true, + classes: ['slazero2', 'bpn-tracker-window'] + }); + } + + static open() { + if (!SLABPNTracker._inst) SLABPNTracker._inst = new SLABPNTracker(); + SLABPNTracker._inst.render(true); + return SLABPNTracker._inst; + } + + _pcActors() { + return (game.actors ?? []).filter(a => a.type === 'character' && a.system?.details?.isPlayerCharacter); + } + + _groupByBPN(actors) { + const groups = new Map(); + for (const actor of actors) { + const code = actor.system?.bpn?.code?.trim() || ''; + const key = code === '' ? '__unassigned__' : code; + if (!groups.has(key)) groups.set(key, []); + groups.get(key).push(actor); + } + return groups; + } + + _bpnTypeColors() { + return { + 'Blue': { bg: '#1a3a6e', border: '#3399ff', text: '#88ccff' }, + 'White': { bg: '#3a3a3a', border: '#cccccc', text: '#ffffff' }, + 'Green': { bg: '#1a3a1a', border: '#44cc66', text: '#88ff88' }, + 'Red': { bg: '#3a1a1a', border: '#cc2222', text: '#ff6666' }, + 'Black': { bg: '#0d0d0d', border: '#555555', text: '#aaaaaa' }, + 'Silver': { bg: '#2a2a3a', border: '#aaaacc', text: '#ccccff' } + }; + } + + _buildTypeBadge(bpnType) { + const colors = this._bpnTypeColors(); + const c = colors[bpnType] ?? { bg: '#222', border: '#666', text: '#ccc' }; + return `${bpnType || 'Unknown'}`; + } + + _buildStatusBadge(status) { + const cls = { + 'Active': 'bpn-status-active', + 'Pending': 'bpn-status-pending', + 'Complete': 'bpn-status-complete', + 'Failed': 'bpn-status-failed' + }[status] ?? 'bpn-status-pending'; + return `${status || 'Pending'}`; + } + + _buildMissionCard(code, actors) { + const rep = actors[0]; + const bpn = rep.system?.bpn ?? {}; + const bpnType = bpn.type || 'Unknown'; + const status = bpn.status || 'Pending'; + const reward = bpn.reward ?? 0; + const description = bpn.description || ''; + const objectives = bpn.objectives || ''; + + const colors = this._bpnTypeColors(); + const c = colors[bpnType] ?? { bg: '#1a1a1a', border: '#555', text: '#aaa' }; + + const actorIds = actors.map(a => a.id).join(','); + + const pcBadges = actors.map(a => + `${a.name}` + ).join(''); + + const statusOptions = ['Pending', 'Active', 'Complete', 'Failed'].map(s => + `` + ).join(''); + + const typeOptions = ['Blue', 'White', 'Green', 'Red', 'Black', 'Silver', 'Unknown'].map(t => + `` + ).join(''); + + return ` +
+
+ ${code} + ${this._buildTypeBadge(bpnType)} + ${this._buildStatusBadge(status)} + +
+ +
+ + + + +
+ +
+ 💳 Reward: ${reward}¢ + +
+ +
+ + +
+ +
+ + +
+ + +
`; + } + + _buildAssignPanel(actors) { + const pcCheckboxes = actors.map(a => + `` + ).join(''); + + const typeOptions = ['Blue', 'White', 'Green', 'Red', 'Black', 'Silver'].map(t => + `` + ).join(''); + + const statusOptions = ['Pending', 'Active', 'Complete', 'Failed'].map(s => + `` + ).join(''); + + return ` +
+
Assign BPN
+
+
+ +
${pcCheckboxes}
+
+
+
+ + +
+
+ + + + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+
+
`; + } + + async _renderInner() { + const actors = this._pcActors(); + const groups = this._groupByBPN(actors); + + let cardsHtml = ''; + + // Render assigned BPNs first (all non-unassigned keys) + for (const [key, group] of groups.entries()) { + if (key === '__unassigned__') continue; + cardsHtml += this._buildMissionCard(key, group); + } + + // Render unassigned section + const unassigned = groups.get('__unassigned__') ?? []; + let unassignedHtml = ''; + if (unassigned.length > 0) { + const badges = unassigned.map(a => `${a.name}`).join(''); + unassignedHtml = ` +
+
Unassigned PCs
+
${badges}
+
`; + } + + const assignPanel = this._buildAssignPanel(actors); + + const html = ` +
+
+ ${cardsHtml || '
No active BPNs. Assign one below.
'} +
+ ${unassignedHtml} + ${assignPanel} +
`; + + return $(html); + } + + async _render(force, options) { + await super._render(force, options); + if (!this._hooks.length) { + const refresh = () => { if (this.rendered) this.render(false); }; + this._hooks.push(Hooks.on('updateActor', refresh)); + this._hooks.push(Hooks.on('createActor', refresh)); + this._hooks.push(Hooks.on('deleteActor', refresh)); + } + } + + async close(options) { + this._hooks.forEach(id => Hooks.off('updateActor', id)); + this._hooks = []; + SLABPNTracker._inst = null; + return super.close(options); + } + + activateListeners(html) { + super.activateListeners(html); + + // Status change — update all PCs on this BPN + html.on('change', '.bpn-status-select', async (ev) => { + const card = ev.currentTarget.closest('.bpn-mission-card'); + const actorIds = card.dataset.actorIds.split(',').filter(Boolean); + const newStatus = ev.currentTarget.value; + for (const id of actorIds) { + const actor = game.actors.get(id); + if (!actor) continue; + try { + await actor.update({ 'system.bpn.status': newStatus }); + } catch (err) { + console.error(`BPN Tracker | Failed to update status for ${actor.name}:`, err); + } + } + // Update the badge in-place without full re-render + const badge = card.querySelector('.bpn-status-badge'); + if (badge) { + badge.className = 'bpn-status-badge ' + ({ + 'Active': 'bpn-status-active', + 'Pending': 'bpn-status-pending', + 'Complete': 'bpn-status-complete', + 'Failed': 'bpn-status-failed' + }[newStatus] ?? 'bpn-status-pending'); + badge.textContent = newStatus; + } + ui.notifications.info(`BPN status updated to "${newStatus}".`); + }); + + // Type change — update all PCs on this BPN + html.on('change', '.bpn-type-select', async (ev) => { + const card = ev.currentTarget.closest('.bpn-mission-card'); + const actorIds = card.dataset.actorIds.split(',').filter(Boolean); + const newType = ev.currentTarget.value; + const colors = this._bpnTypeColors(); + const c = colors[newType] ?? { bg: '#222', border: '#666', text: '#ccc' }; + for (const id of actorIds) { + const actor = game.actors.get(id); + if (!actor) continue; + try { + await actor.update({ 'system.bpn.type': newType }); + } catch (err) { + console.error(`BPN Tracker | Failed to update type for ${actor.name}:`, err); + } + } + // Update badge and border color in-place + const badge = card.querySelector('.bpn-type-badge'); + if (badge) { + badge.style.background = c.bg; + badge.style.borderColor = c.border; + badge.style.color = c.text; + badge.textContent = newType; + } + card.style.borderLeftColor = c.border; + ui.notifications.info(`BPN type updated to "${newType}".`); + }); + + // Pay Out — add reward to each PC's credits + html.on('click', '[data-action="payout"]', async (ev) => { + const card = ev.currentTarget.closest('.bpn-mission-card'); + const actorIds = card.dataset.actorIds.split(',').filter(Boolean); + let paid = 0; + for (const id of actorIds) { + const actor = game.actors.get(id); + if (!actor) continue; + const reward = actor.system?.bpn?.reward ?? 0; + const cur = actor.system?.details?.credits ?? 0; + try { + await actor.update({ 'system.details.credits': cur + reward }); + paid++; + } catch (err) { + console.error(`BPN Tracker | Failed to pay out reward to ${actor.name}:`, err); + } + } + ui.notifications.info(`Reward paid out to ${paid} PC(s).`); + }); + + // Save description + objectives back to all PCs on this BPN + html.on('click', '[data-action="save-bpn"]', async (ev) => { + const card = ev.currentTarget.closest('.bpn-mission-card'); + const actorIds = card.dataset.actorIds.split(',').filter(Boolean); + const description = card.querySelector('.bpn-description').value; + const objectives = card.querySelector('.bpn-objectives').value; + for (const id of actorIds) { + const actor = game.actors.get(id); + if (!actor) continue; + try { + await actor.update({ + 'system.bpn.description': description, + 'system.bpn.objectives': objectives + }); + } catch (err) { + console.error(`BPN Tracker | Failed to save BPN fields for ${actor.name}:`, err); + } + } + ui.notifications.info('BPN description and objectives saved.'); + }); + + // Clear BPN — remove code and status from all PCs on this BPN + html.on('click', '[data-action="clear-bpn"]', async (ev) => { + const card = ev.currentTarget.closest('.bpn-mission-card'); + const actorIds = card.dataset.actorIds.split(',').filter(Boolean); + for (const id of actorIds) { + const actor = game.actors.get(id); + if (!actor) continue; + try { + await actor.update({ 'system.bpn.code': '', 'system.bpn.status': '' }); + } catch (err) { + console.error(`BPN Tracker | Failed to clear BPN for ${actor.name}:`, err); + } + } + ui.notifications.info('BPN cleared from assigned PCs.'); + if (this.rendered) this.render(false); + }); + + // Assign BPN — write BPN data to selected PCs + html.on('click', '[data-action="assign-bpn"]', async (ev) => { + const panel = ev.currentTarget.closest('.bpn-assign-panel'); + const selectedIds = [...panel.querySelectorAll('.bpn-assign-pc-check:checked')].map(el => el.value); + if (selectedIds.length === 0) { + ui.notifications.warn('No PCs selected for BPN assignment.'); + return; + } + const code = panel.querySelector('.bpn-assign-code').value.trim(); + if (!code) { + ui.notifications.warn('Please enter a BPN code.'); + return; + } + const bpnType = panel.querySelector('.bpn-assign-type').value; + const status = panel.querySelector('.bpn-assign-status').value; + const reward = Number(panel.querySelector('.bpn-assign-reward').value) || 0; + const description = panel.querySelector('.bpn-assign-desc').value; + const objectives = panel.querySelector('.bpn-assign-obj').value; + + for (const id of selectedIds) { + const actor = game.actors.get(id); + if (!actor) continue; + try { + await actor.update({ + 'system.bpn.code': code, + 'system.bpn.type': bpnType, + 'system.bpn.status': status, + 'system.bpn.reward': reward, + 'system.bpn.description': description, + 'system.bpn.objectives': objectives + }); + } catch (err) { + console.error(`BPN Tracker | Failed to assign BPN to ${actor.name}:`, err); + } + } + ui.notifications.info(`BPN "${code}" assigned to ${selectedIds.length} PC(s).`); + + // Clear the form + panel.querySelector('.bpn-assign-code').value = ''; + panel.querySelector('.bpn-assign-reward').value = '0'; + panel.querySelector('.bpn-assign-desc').value = ''; + panel.querySelector('.bpn-assign-obj').value = ''; + panel.querySelectorAll('.bpn-assign-pc-check').forEach(el => el.checked = false); + + if (this.rendered) this.render(false); + }); + } +} + +SLABPNTracker._inst = null; + +// ── ⚡ CONDITION APPLICATOR ──────────────────────────────────────────────────────────── +class SLAConditionApplicator extends Application { + constructor(...args) { + super(...args); + this._hooks = []; + this._selectedActorId = null; + this._allPCsMode = false; + } + + static get defaultOptions() { + return foundry.utils.mergeObject(super.defaultOptions, { + id: 'sla-condition-applicator', + title: '⚡ Condition Applicator', + width: 600, + height: 520, + resizable: true, + classes: ['slazero2', 'condition-applicator-window'] + }); + } + + static open() { + if (!SLAConditionApplicator._inst) SLAConditionApplicator._inst = new SLAConditionApplicator(); + SLAConditionApplicator._inst.render(true); + return SLAConditionApplicator._inst; + } + + _pcActors() { + return (game.actors ?? []).filter(a => a.type === 'character' && a.system?.details?.isPlayerCharacter); + } + + async _renderInner() { + const pcs = this._pcActors(); + + // Default selected actor to first PC if none set or stale + if (!this._selectedActorId || !pcs.find(a => a.id === this._selectedActorId)) { + this._selectedActorId = pcs.length ? pcs[0].id : null; + } + + const selActor = pcs.find(a => a.id === this._selectedActorId) ?? null; + + // --- Build PC roster rows --- + let pcRowsHtml = ''; + for (const actor of pcs) { + const activeConditions = SLA_CONDITIONS.filter(c => actor.statuses?.has(c.id)); + const dotBadges = activeConditions.map(c => + `` + ).join(''); + const isSelected = actor.id === this._selectedActorId && !this._allPCsMode; + pcRowsHtml += ` +
+ ${actor.name} + ${dotBadges} +
`; + } + if (!pcRowsHtml) { + pcRowsHtml = '
No PC actors found.
'; + } + + // --- Build condition grid tiles --- + let condTilesHtml = ''; + for (const cond of SLA_CONDITIONS) { + let isActive = false; + if (this._allPCsMode) { + // Active if ALL PCs have it (useful visual: show partial state too) + const pcCount = pcs.length; + const hasCount = pcs.filter(a => a.statuses?.has(cond.id)).length; + if (hasCount === pcCount && pcCount > 0) isActive = true; + } else if (selActor) { + isActive = selActor.statuses?.has(cond.id) ?? false; + } + + // Build dice modifier badge text + let modBadge = ''; + if (cond.diceModifiers && Object.keys(cond.diceModifiers).length) { + const parts = Object.entries(cond.diceModifiers).map(([key, val]) => { + const sign = val >= 0 ? '+' : '−'; + const abs = Math.abs(val); + return `${sign}${abs} ${key}`; + }); + modBadge = `${parts.join(' ')}`; + } + + const descAttr = cond.description ? ` title="${cond.description.replace(/"/g, '"')}"` : ''; + + condTilesHtml += ` +
+ ${modBadge} + + ${cond.label} +
`; + } + if (!condTilesHtml) { + condTilesHtml = '
No conditions defined.
'; + } + + // --- Active conditions status bar --- + let statusBarHtml = ''; + if (this._allPCsMode) { + statusBarHtml = 'All PCs mode active'; + } else if (selActor) { + const activeConds = SLA_CONDITIONS.filter(c => selActor.statuses?.has(c.id)); + if (activeConds.length) { + statusBarHtml = activeConds.map(c => + ` ${c.label}` + ).join(''); + } else { + statusBarHtml = 'No active conditions'; + } + } else { + statusBarHtml = 'No PC selected'; + } + + const selectedLabel = this._allPCsMode + ? 'All PCs' + : (selActor ? selActor.name : '—'); + + const html = ` +
+
+
+ Target: + ${selectedLabel} +
+
+ + +
+
+ +
+
+
PC Roster
+
+ ${pcRowsHtml} +
+
+
+
+ ${condTilesHtml} +
+
+
+ +
+ Active: +
+ ${statusBarHtml} +
+
+
`; + + return $(html); + } + + async _render(force, options) { + await super._render(force, options); + if (!this._hooks.length) { + const refresh = () => { if (this.rendered) this.render(false); }; + this._hooks.push( + Hooks.on('updateActor', refresh), + Hooks.on('createActiveEffect', refresh), + Hooks.on('deleteActiveEffect', refresh), + Hooks.on('updateActiveEffect', refresh) + ); + } + } + + async close(options) { + const evts = ['updateActor', 'createActiveEffect', 'deleteActiveEffect', 'updateActiveEffect']; + this._hooks.forEach((id, i) => Hooks.off(evts[i], id)); + this._hooks = []; + SLAConditionApplicator._inst = null; + return super.close(options); + } + + activateListeners(html) { + super.activateListeners(html); + + // PC row click — select actor + html.find('.cap-pc-row').on('click', (ev) => { + const actorId = ev.currentTarget.dataset.actorId; + if (actorId) { + this._selectedActorId = actorId; + this._allPCsMode = false; + this.render(false); + } + }); + + // Condition tile click — toggle condition + html.find('.cap-cond-tile').on('click', async (ev) => { + const condId = ev.currentTarget.dataset.condId; + const isActive = ev.currentTarget.dataset.active === 'true'; + const pcs = this._pcActors(); + + if (this._allPCsMode) { + for (const actor of pcs) { + try { + await actor.toggleStatusEffect(condId, { active: !isActive }); + } catch (err) { + console.error(`SLAConditionApplicator | Failed to toggle ${condId} on ${actor.name}:`, err); + ui.notifications.warn(`Could not toggle condition on ${actor.name}.`); + } + } + const verb = isActive ? 'Removed' : 'Applied'; + const condLabel = SLA_CONDITIONS.find(c => c.id === condId)?.label ?? condId; + ui.notifications.info(`${verb} "${condLabel}" on all PCs.`); + } else { + const selActor = pcs.find(a => a.id === this._selectedActorId); + if (!selActor) { + ui.notifications.warn('No PC selected.'); + return; + } + try { + await selActor.toggleStatusEffect(condId, { active: !isActive }); + const verb = isActive ? 'Removed' : 'Applied'; + const condLabel = SLA_CONDITIONS.find(c => c.id === condId)?.label ?? condId; + ui.notifications.info(`${verb} "${condLabel}" on ${selActor.name}.`); + } catch (err) { + console.error(`SLAConditionApplicator | Failed to toggle ${condId} on ${selActor.name}:`, err); + ui.notifications.warn(`Could not toggle condition on ${selActor.name}.`); + } + } + }); + + // Toggle All PCs mode + html.find('[data-action="toggle-all-pcs"]').on('click', () => { + this._allPCsMode = !this._allPCsMode; + this.render(false); + }); + + // Clear All conditions + html.find('[data-action="clear-all"]').on('click', async () => { + const pcs = this._pcActors(); + const targets = this._allPCsMode + ? pcs + : pcs.filter(a => a.id === this._selectedActorId); + + if (!targets.length) { + ui.notifications.warn('No PC selected.'); + return; + } + + for (const actor of targets) { + for (const cond of SLA_CONDITIONS) { + if (actor.statuses?.has(cond.id)) { + try { + await actor.toggleStatusEffect(cond.id, { active: false }); + } catch (err) { + console.error(`SLAConditionApplicator | Failed to clear ${cond.id} on ${actor.name}:`, err); + } + } + } + } + + const targetLabel = this._allPCsMode ? 'all PCs' : (targets[0]?.name ?? 'selected PC'); + ui.notifications.info(`Cleared all conditions from ${targetLabel}.`); + }); + } +} + +SLAConditionApplicator._inst = null; + +// ── ACTOR DIRECTORY BUTTONS ────────────────────────────────────────────────── +Hooks.on('renderActorDirectory', (app, html) => { + const root = html instanceof jQuery ? html[0] : html; + const actions = root.querySelector('.header-actions') || root.querySelector('.directory-header'); + if (!actions) return; + + // Avoid duplicates on re-render + if (actions.querySelector('.sla-gen-pc-btn')) return; + + const pcBtn = document.createElement('button'); + pcBtn.className = 'sla-gen-pc-btn'; + pcBtn.innerHTML = ' Generate PC'; + pcBtn.style.cssText = 'font-size:11px;padding:2px 7px;margin-left:4px;background:rgba(255,102,0,0.2);border:1px solid #ff6600;color:#ff6600;border-radius:3px;cursor:pointer;'; + pcBtn.addEventListener('click', () => new SLAPCCreator().render(true)); + + const npcBtn = document.createElement('button'); + npcBtn.className = 'sla-gen-npc-btn'; + npcBtn.innerHTML = ' Generate NPC'; + npcBtn.style.cssText = 'font-size:11px;padding:2px 7px;margin-left:4px;background:rgba(0,200,200,0.1);border:1px solid #00aacc;color:#00aacc;border-radius:3px;cursor:pointer;'; + npcBtn.addEventListener('click', () => new SLANPCCreator().render(true)); + + // GM-only Finance Ledger button + if (game.user?.isGM) { + const finBtn = document.createElement('button'); + finBtn.className = 'sla-gm-finance-btn'; + finBtn.innerHTML = ' Finance Ledger'; + finBtn.style.cssText = 'font-size:11px;padding:2px 7px;margin-left:4px;background:rgba(255,200,0,0.15);border:1px solid #ffcc00;color:#ffcc00;border-radius:3px;cursor:pointer;'; + finBtn.addEventListener('click', () => { + if (!game.slaGMFinanceTool) game.slaGMFinanceTool = new SLAGMFinanceTool(); + game.slaGMFinanceTool.render(true); + }); + actions.append(finBtn); + + const grpBtn = document.createElement('button'); + grpBtn.className = 'sla-group-npc-btn'; + grpBtn.innerHTML = ' Group NPCs'; + grpBtn.style.cssText = 'font-size:11px;padding:2px 7px;margin-left:4px;background:rgba(100,200,80,0.15);border:1px solid #66cc44;color:#66cc44;border-radius:3px;cursor:pointer;'; + grpBtn.addEventListener('click', () => { + if (!game.slaGroupNPCTool) game.slaGroupNPCTool = new SLAGroupNPCTool(); + game.slaGroupNPCTool.render(true); + }); + actions.append(grpBtn); + + // ── SLA GM Tool buttons ───────────────────────────────────────────────── + const gmToolDefs = [ + { cls: 'sla-dir-status-btn', icon: 'fas fa-clipboard-list', label: 'PC Status', color: '#00d4ff', fn: () => SLAGMStatusWindow.open() }, + { cls: 'sla-dir-credit-btn', icon: 'fas fa-credit-card', label: 'Credits', color: '#00d4ff', fn: () => SLACreditDistributionTool.open() }, + { cls: 'sla-dir-ledger-btn', icon: 'fas fa-book-open', label: 'Ledger', color: '#aa88ff', fn: () => SLAShiftLedger.open() }, + { cls: 'sla-dir-threat-btn', icon: 'fas fa-skull', label: 'Threats', color: '#ff4444', fn: () => SLANPCThreatBoard.open() }, + { cls: 'sla-dir-bpn-btn', icon: 'fas fa-clipboard-check', label: 'BPN', color: '#ffcc00', fn: () => SLABPNTracker.open() }, + { cls: 'sla-dir-condapp-btn', icon: 'fas fa-bolt', label: 'Conditions', color: '#ff6b35', fn: () => SLAConditionApplicator.open() }, + ]; + for (const def of gmToolDefs) { + if (actions.querySelector(`.${def.cls}`)) continue; + const btn = document.createElement('button'); + btn.className = def.cls; + btn.innerHTML = ` ${def.label}`; + btn.style.cssText = `font-size:11px;padding:2px 6px;margin-left:3px;background:rgba(0,0,0,0.3);border:1px solid ${def.color};color:${def.color};border-radius:3px;cursor:pointer;`; + btn.addEventListener('click', def.fn); + actions.append(btn); + } + } + + actions.append(pcBtn, npcBtn); +}); + + +// ══════════════════════════════════════════════════════════════════════════════ +// SLA INDUSTRIES — RACIAL ABILITIES +// Auto-embedded specialty items with `category: "racial"` and `racialBonuses` +// array driving automatic dice-pool modifiers via _getRacialDiceBonus(). +// ══════════════════════════════════════════════════════════════════════════════ + +const SLA_RACIAL_ABILITY_DATA = { + + stormer: [ + { + name: "Natural Weapons (Claws & Teeth)", + type: "specialty", + img: "systems/slazero2/assets/racial/natural-weapons.svg", + system: { + category: "racial", + description: "Vatgrown bio-weapons. Always counts as armed. +1 die on all melee attack rolls.", + effects: "+1 die on melee rolls. Always treated as armed in close combat.", + racialBonuses: [{ skill: "melee", bonus: 1 }], + prerequisites: "Stormer 313-S Malice", + isActive: true, healthMod: 0, resolveMod: 0 + } + }, + { + name: "Stormer Regeneration", + type: "specialty", + img: "systems/slazero2/assets/racial/regen.svg", + system: { + category: "racial", + description: "Enhanced biology heals wound tissue rapidly. Recover 1 HP per hour of rest.", + effects: "Recover 1 HP per hour of rest. Does not function at 0 HP.", + racialBonuses: [], + prerequisites: "Stormer 313-S Malice", + isActive: true, healthMod: 0, resolveMod: 0, + regenHpPerHour: 1 + } + } + ], + + shaktar: [ + { + name: "Battle Claws", + type: "specialty", + img: "systems/slazero2/assets/racial/battle-claws.svg", + system: { + category: "racial", + description: "Retractable duranium-hard claws. Always armed. +1 die on melee rolls.", + effects: "+1 die on all melee attack rolls. Always treated as armed.", + racialBonuses: [{ skill: "melee", bonus: 1 }], + prerequisites: "Shaktar", + isActive: true, healthMod: 0, resolveMod: 0 + } + }, + { + name: "Warrior Caste Training", + type: "specialty", + img: "systems/slazero2/assets/racial/warrior-caste.svg", + system: { + category: "racial", + description: "Generations of ritual melee combat. +1 additional die on melee rolls when wielding any melee weapon.", + effects: "+1 die on melee rolls when using a melee weapon (stacks with Battle Claws for +2 total).", + racialBonuses: [{ skill: "melee", bonus: 1 }], + prerequisites: "Shaktar", + isActive: true, healthMod: 0, resolveMod: 0 + } + } + ], + + wraithraider: [ + { + name: "Shaper Senses", + type: "specialty", + img: "systems/slazero2/assets/racial/shaper-senses.svg", + system: { + category: "racial", + description: "Heightened multi-spectrum perception. +2 dice on Observation rolls. +1 die on Stealth rolls.", + effects: "+2 dice on Observation rolls. +1 die on Stealth rolls.", + racialBonuses: [ + { skill: "observation", bonus: 2 }, + { skill: "stealth", bonus: 1 } + ], + prerequisites: "Wraith Raider", + isActive: true, healthMod: 0, resolveMod: 0 + } + }, + { + name: "Wraith Teeth & Claw", + type: "specialty", + img: "systems/slazero2/assets/racial/natural-weapons.svg", + system: { + category: "racial", + description: "Bone-hard teeth and razor claws grown through decades of cold-world adaptation. Natural melee weapons — Damage 2, AP 1, ROF 2. Always counts as armed.", + effects: "Natural weapon: DMG 2, AP 1, ROF 2. Always armed.", + racialBonuses: [], + prerequisites: "Wraith Raider", + isActive: true, healthMod: 0, resolveMod: 0, + isNaturalWeapon: true, naturalWeaponDamage: 2, naturalWeaponAP: 1, naturalWeaponROF: 2 + } + } + ], + + frother: [ + { + name: "Blade Proficiency", + type: "specialty", + img: "systems/slazero2/assets/racial/blade-proficiency.svg", + system: { + category: "racial", + description: "Frothers are culturally and chemically attuned to bladed combat. +2 dice on melee rolls with bladed or sword-type weapons.", + effects: "+2 dice on melee rolls when using bladed weapons (swords, knives, power blades).", + racialBonuses: [{ skill: "melee", bonus: 2, weaponTag: "blade" }, + { skill: "melee", bonus: 2, weaponTag: "knife" }], + prerequisites: "Frother", + isActive: true, healthMod: 0, resolveMod: 0 + } + }, + { + name: "Drug Dependency", + type: "specialty", + img: "systems/slazero2/assets/racial/drug-dependency.svg", + system: { + category: "racial", + description: "A Frother's biochemistry depends on combat stimulants. Without an active drug: −1 die to all rolls (withdrawal).", + effects: "−1 to all rolls when no drug is active (withdrawal). Drug bonuses apply normally when active.", + racialBonuses: [], + prerequisites: "Frother", + isActive: true, healthMod: 0, resolveMod: 0, + drugDependent: true + } + } + ], + + human: [ + { + name: "Social Versatility", + type: "specialty", + img: "systems/slazero2/assets/racial/social-versatility.svg", + system: { + category: "racial", + description: "Humans navigate SLA's corporate social structures with natural ease. +1 die on Persuasion and Insight rolls.", + effects: "+1 die on Persuasion rolls. +1 die on Insight rolls.", + racialBonuses: [ + { skill: "persuasion", bonus: 1 }, + { skill: "insight", bonus: 1 } + ], + prerequisites: "Human", + isActive: true, healthMod: 0, resolveMod: 0 + } + } + ], + + vevaphon: [ + { + name: "Brute Form", + type: "specialty", img: "systems/slazero2/assets/racial/warrior-caste.svg", + system: { + category: "racial", description: "Dense armoured combat chassis. +2 STR, +2 BODY equivalent (+2 Health max, ignores knockdown). Cannot be disguised. Costs 1 Instability to activate.", + effects: "+2 effective Strength dice, +2 Health max, Knockdown immune. Costs 1 Instability.", + racialBonuses: [{ skill: "force", bonus: 1 }, { skill: "melee", bonus: 1 }], + prerequisites: "Vevaphon", isActive: false, healthMod: 2, resolveMod: 0 + } + }, + { + name: "Stalker Form", + type: "specialty", img: "systems/slazero2/assets/racial/shaper-senses.svg", + system: { + category: "racial", description: "Lean predatory chassis for silence and ambush. +2 Stealth dice, +1 Agility effective. Cannot wear armour while active. Costs 1 Instability to activate.", + effects: "+2 Stealth, +1 Agility dice. Cannot wear armour. Costs 1 Instability.", + racialBonuses: [{ skill: "stealth", bonus: 2 }, { skill: "mobility", bonus: 1 }], + prerequisites: "Vevaphon", isActive: false, healthMod: 0, resolveMod: 0 + } + }, + { + name: "Raptor Form", + type: "specialty", img: "systems/slazero2/assets/racial/battle-claws.svg", + system: { + category: "racial", description: "Strike-optimised chassis with natural weapons. +2 Melee dice, +1 Marksmanship (close range only). Critical hits deal +1 damage. Costs 1 Instability to activate.", + effects: "+2 Melee, +1 Marksmanship (close range). Crits deal +1 damage. Costs 1 Instability.", + racialBonuses: [{ skill: "melee", bonus: 2 }, { skill: "marksmanship", bonus: 1 }], + prerequisites: "Vevaphon", isActive: false, healthMod: 0, resolveMod: 0 + } + }, + { + name: "Morphic Strike", + type: "specialty", img: "systems/slazero2/assets/racial/natural-weapons.svg", + system: { + category: "racial", description: "Natural biogenetic weapon formed from the Vevaphon's own mass. Damage 2, AP 1, ROF 2. Always available regardless of morph form. Counts as melee weapon for skill rolls.", + effects: "Natural weapon: DMG 2, AP 1, ROF 2. Always available.", + racialBonuses: [], + prerequisites: "Vevaphon", isActive: true, healthMod: 0, resolveMod: 0, + isNaturalWeapon: true, naturalWeaponDamage: 2, naturalWeaponAP: 1, naturalWeaponROF: 2 + } + } + ] +}; + +// ─── Auto-embed racial abilities ───────────────────────────────────────────── + +async function _ensureRacialAbilities(actor) { + if (!actor || actor.type !== "character") return; + const race = String(actor.system?.race || "") + .toLowerCase() + .replace(/[\s-]/g, "") + .replace("313-s", "") + .trim(); + + const abilities = SLA_RACIAL_ABILITY_DATA[race]; + if (!abilities || !abilities.length) return; + + const toCreate = []; + const toUpdate = []; + for (const ability of abilities) { + const existing = actor.items.find( + i => i.type === "specialty" && i.name === ability.name + ); + if (!existing) { + toCreate.push(ability); + } else { + const patch = {}; + if (ability.img && existing.img !== ability.img) patch.img = ability.img; + // Patch natural weapon fields if missing or stale + if (ability.system?.isNaturalWeapon && !existing.system?.isNaturalWeapon) { + patch["system.isNaturalWeapon"] = true; + patch["system.naturalWeaponDamage"] = ability.system.naturalWeaponDamage ?? 2; + patch["system.naturalWeaponAP"] = ability.system.naturalWeaponAP ?? 1; + patch["system.naturalWeaponROF"] = ability.system.naturalWeaponROF ?? 2; + } + if (Object.keys(patch).length) toUpdate.push({ _id: existing.id, ...patch }); + } + } + if (toUpdate.length) { + try { await actor.updateEmbeddedDocuments("Item", toUpdate); } catch(_) {} + } + if (!toCreate.length) return; + + try { + await actor.createEmbeddedDocuments("Item", toCreate); + ui.notifications.info( + `${actor.name}: racial abilities embedded (${toCreate.map(a => a.name).join(", ")})` + ); + } catch (err) { + console.warn("SLA Zero Two | Failed to embed racial abilities:", err); + } +} + +// ─── Hooks ─────────────────────────────────────────────────────────────────── + +// Auto-embed when a new actor is created +Hooks.on("createActor", async (actor) => { + if (!game.user?.isGM) return; + await _ensureRacialAbilities(actor); +}); + +// On boot, ensure all existing character actors have their racial abilities +Hooks.once("ready", function() { + if (!game.user?.isGM) return; + (async () => { + let count = 0; + for (const actor of game.actors ?? []) { + if (actor.type !== "character") continue; + const race = String(actor.system?.race || "").toLowerCase(); + if (!race || race === "undefined") continue; + const before = actor.items.filter(i => i.system?.category === "racial").length; + await _ensureRacialAbilities(actor); + const after = actor.items.filter(i => i.system?.category === "racial").length; + if (after > before) count++; + } + if (count > 0) console.log(`SLA Zero Two | Embedded racial abilities on ${count} actor(s).`); + })(); +}); + +// Stormer Regeneration: heal 1 HP every real-time hour (GM only) +// Uses in-game time advancement via updateWorldTime hook +Hooks.on("updateWorldTime", async (worldTime, delta) => { + if (!game.user?.isGM) return; + // Only trigger on meaningful time advances (≥ 3600 seconds = 1 hour) + if (Math.abs(delta) < 3600) return; + const hoursElapsed = Math.floor(Math.abs(delta) / 3600); + if (hoursElapsed < 1) return; + + for (const actor of game.actors ?? []) { + if (actor.type !== "character") continue; + if (!actor.items.some(i => i.name === "Stormer Regeneration" && i.type === "specialty")) continue; + + const hp = Number(actor.system?.derivedStats?.health?.value ?? 0); + const hpMax = Number(actor.system?.derivedStats?.health?.max ?? 0); + if (hp <= 0 || hp >= hpMax) continue; + + const heal = Math.min(hoursElapsed, hpMax - hp); + const newHp = hp + heal; + await actor.update({ "system.derivedStats.health.value": newHp }); + ChatMessage.create({ + speaker: { alias: actor.name }, + content: `
⚡ Stormer Regeneration
+ ${actor.name} regenerates +${heal} HP (${hp} → ${newHp}/${hpMax}).
` + }); + } +}); + + +// ══════════════════════════════════════════════════════════════════════════════ +// SLA INDUSTRIES — AMMO SYSTEM +// Caliber-based ammunition with Standard / AP / HE rounds. +// Bullet Tax: credits are deducted when rounds are fired. +// ══════════════════════════════════════════════════════════════════════════════ + +const SLA_AMMO_CATALOG = { + // ── Pistol calibres ────────────────────────────────────────────────────── + "10mm": { + label: "10mm Pistol", + fitWeaponTypes: ["pistol", "pistol smg"], + magazineDefault: 15, + rounds: { + standard: { name: "10mm Standard", costPerRound: 5, apBonus: 0, damageMod: 0, blastRadius: 0, description: "Standard ball round. Reliable, cheap." }, + ap: { name: "10mm Armor Piercing", costPerRound: 15, apBonus: 2, damageMod: 0, blastRadius: 0, description: "+2 AP. Penetrates light corporate armor." }, + he: { name: "10mm High Explosive", costPerRound: 35, apBonus: 0, damageMod: 1, blastRadius: 0, description: "+1 damage on impact. Fragmentation effect." } + } + }, + ".44": { + label: ".44 Revolver", + fitWeaponTypes: ["revolver"], + magazineDefault: 6, + rounds: { + standard: { name: ".44 Standard", costPerRound: 8, apBonus: 0, damageMod: 0, blastRadius: 0, description: "Standard heavy revolver round." }, + ap: { name: ".44 Armor Piercing", costPerRound: 24, apBonus: 2, damageMod: 0, blastRadius: 0, description: "+2 AP. Hardened penetrator." }, + he: { name: ".44 High Explosive", costPerRound: 45, apBonus: 0, damageMod: 1, blastRadius: 0, description: "+1 damage. Cavity round." } + } + }, + ".454": { + label: ".454 Heavy Pistol", + fitWeaponTypes: ["pistol"], + magazineDefault: 12, + rounds: { + standard: { name: ".454 Standard", costPerRound: 10, apBonus: 0, damageMod: 0, blastRadius: 0, description: "Large-bore semi-auto pistol round." }, + ap: { name: ".454 Armor Piercing", costPerRound: 28, apBonus: 2, damageMod: 0, blastRadius: 0, description: "+2 AP. Corporate armor buster." }, + he: { name: ".454 High Explosive", costPerRound: 50, apBonus: 0, damageMod: 1, blastRadius: 0, description: "+1 damage. Devastating at close range." } + } + }, + ".50": { + label: ".50 Heavy Pistol", + fitWeaponTypes: ["pistol"], + magazineDefault: 8, + rounds: { + standard: { name: ".50 Standard", costPerRound: 12, apBonus: 0, damageMod: 0, blastRadius: 0, description: "Anti-armor pistol cartridge." }, + ap: { name: ".50 Armor Piercing", costPerRound: 35, apBonus: 3, damageMod: 0, blastRadius: 0, description: "+3 AP. Punches through vehicle glass." }, + he: { name: ".50 High Explosive", costPerRound: 60, apBonus: 0, damageMod: 2, blastRadius: 0, description: "+2 damage. Explosive tip." } + } + }, + // ── SMG calibres ───────────────────────────────────────────────────────── + "9mm": { + label: "9mm SMG", + fitWeaponTypes: ["smg", "machine pistol"], + magazineDefault: 30, + rounds: { + standard: { name: "9mm Standard", costPerRound: 4, apBonus: 0, damageMod: 0, blastRadius: 0, description: "High-velocity SMG round." }, + ap: { name: "9mm Armor Piercing", costPerRound: 12, apBonus: 1, damageMod: 0, blastRadius: 0, description: "+1 AP. Drill-tip penetrator." }, + he: { name: "9mm High Explosive", costPerRound: 28, apBonus: 0, damageMod: 1, blastRadius: 0, description: "+1 damage. Micro-charge detonation." } + } + }, + "3mm": { + label: "3mm Machine Pistol", + fitWeaponTypes: ["machine pistol"], + magazineDefault: 20, + rounds: { + standard: { name: "3mm Standard", costPerRound: 3, apBonus: 0, damageMod: 0, blastRadius: 0, description: "High-rate tiny caliber round. Cheap, disposable." }, + ap: { name: "3mm Armor Piercing", costPerRound: 9, apBonus: 1, damageMod: 0, blastRadius: 0, description: "+1 AP. Needle penetrator tip." }, + he: { name: "3mm High Explosive", costPerRound: 20, apBonus: 0, damageMod: 1, blastRadius: 0, description: "+1 damage. Micro-explosive." } + } + }, + // ── Assault/Carbine rifle calibres ──────────────────────────────────────── + "5.56mm": { + label: "5.56mm NATO", + fitWeaponTypes: ["rifle", "assault rifle", "carbine"], + magazineDefault: 30, + rounds: { + standard: { name: "5.56mm Standard", costPerRound: 8, apBonus: 0, damageMod: 0, blastRadius: 0, description: "Standard assault rifle round." }, + ap: { name: "5.56mm Armor Piercing", costPerRound: 22, apBonus: 3, damageMod: 0, blastRadius: 0, description: "+3 AP. Military-grade tungsten tip." }, + he: { name: "5.56mm High Explosive", costPerRound: 45, apBonus: 0, damageMod: 2, blastRadius: 1, description: "+2 damage, 1m blast. Detonates on impact." } + } + }, + "7mm": { + label: "7mm SLA Carbine", + fitWeaponTypes: ["carbine", "lmg"], + magazineDefault: 24, + rounds: { + standard: { name: "7mm Standard", costPerRound: 10, apBonus: 0, damageMod: 0, blastRadius: 0, description: "SLA-standard intermediate carbine round." }, + ap: { name: "7mm Armor Piercing", costPerRound: 28, apBonus: 3, damageMod: 0, blastRadius: 0, description: "+3 AP. Composite penetrator." }, + he: { name: "7mm High Explosive", costPerRound: 55, apBonus: 0, damageMod: 2, blastRadius: 1, description: "+2 damage, 1m blast." } + } + }, + // ── Battle rifle / Sniper ───────────────────────────────────────────────── + "7.62mm": { + label: "7.62mm Battle Rifle", + fitWeaponTypes: ["battle rifle", "lmg", "sniper"], + magazineDefault: 20, + rounds: { + standard: { name: "7.62mm Standard", costPerRound: 12, apBonus: 0, damageMod: 0, blastRadius: 0, description: "Heavy battle rifle cartridge." }, + ap: { name: "7.62mm Armor Piercing", costPerRound: 35, apBonus: 4, damageMod: 0, blastRadius: 0, description: "+4 AP. Penetrates medium body armor." }, + he: { name: "7.62mm High Explosive", costPerRound: 70, apBonus: 0, damageMod: 3, blastRadius: 2, description: "+3 damage, 2m blast. Anti-vehicle." } + } + }, + "8mm": { + label: "8mm Sniper", + fitWeaponTypes: ["sniper rifle", "sniper"], + magazineDefault: 10, + rounds: { + standard: { name: "8mm Standard", costPerRound: 14, apBonus: 0, damageMod: 0, blastRadius: 0, description: "High-precision sniper cartridge." }, + ap: { name: "8mm Armor Piercing", costPerRound: 40, apBonus: 4, damageMod: 0, blastRadius: 0, description: "+4 AP. Long-range penetration round." }, + he: { name: "8mm High Explosive", costPerRound: 75, apBonus: 0, damageMod: 3, blastRadius: 0, description: "+3 damage. Detonates inside target." } + } + }, + // ── Shotgun ─────────────────────────────────────────────────────────────── + "12g": { + label: "12-Gauge Shotgun", + fitWeaponTypes: ["shotgun"], + magazineDefault: 8, + rounds: { + standard: { name: "12g Buckshot", costPerRound: 18, apBonus: 0, damageMod: 0, blastRadius: 0, description: "Multi-pellet scatter. Devastating close range." }, + ap: { name: "12g Sabot Slug", costPerRound: 45, apBonus: 2, damageMod: 0, blastRadius: 0, description: "+2 AP. Single hardened slug." }, + he: { name: "12g Frag Shell", costPerRound: 80, apBonus: 0, damageMod: 2, blastRadius: 1, description: "+2 damage, 1m blast. Explosive round." } + } + }, + // ── Heavy weapon calibres ───────────────────────────────────────────────── + "12.7mm": { + label: "12.7mm Heavy Autocannon", + fitWeaponTypes: ["hw", "heavy weapon", "autocannon"], + magazineDefault: 20, + rounds: { + standard: { name: "12.7mm Standard", costPerRound: 25, apBonus: 0, damageMod: 0, blastRadius: 0, description: "Heavy caliber autocannon round." }, + ap: { name: "12.7mm Armor Piercing", costPerRound: 65, apBonus: 5, damageMod: 0, blastRadius: 0, description: "+5 AP. Penetrates vehicle hull." }, + he: { name: "12.7mm High Explosive", costPerRound: 120, apBonus: 0, damageMod: 4, blastRadius: 3, description: "+4 damage, 3m blast. Anti-materiel." } + } + }, + // ── Specialist calibres ─────────────────────────────────────────────────── + "40mm": { + label: "40mm Grenade", + fitWeaponTypes: ["grenade", "launcher"], + magazineDefault: 6, + rounds: { + standard: { name: "40mm Fragmentation", costPerRound: 80, apBonus: 0, damageMod: 0, blastRadius: 3, description: "Standard fragmentation grenade round. 3m blast." }, + ap: { name: "40mm HEAT", costPerRound: 180, apBonus: 5, damageMod: 2, blastRadius: 1, description: "+5 AP, +2 damage. High-explosive anti-tank." }, + he: { name: "40mm Incendiary", costPerRound: 140, apBonus: 0, damageMod: 3, blastRadius: 4, description: "+3 damage, 4m blast. Burns on contact." } + } + }, + "Rail": { + label: "Rail Cannon Slug", + fitWeaponTypes: ["rail gun"], + magazineDefault: 5, + rounds: { + standard: { name: "Rail Slug Standard", costPerRound: 50, apBonus: 0, damageMod: 0, blastRadius: 0, description: "Electromagnetically accelerated tungsten slug." }, + ap: { name: "Rail Slug AP", costPerRound: 120, apBonus: 6, damageMod: 0, blastRadius: 0, description: "+6 AP. Penetrates any personal armor." }, + he: { name: "Rail Slug Explosive", costPerRound: 200, apBonus: 0, damageMod: 5, blastRadius: 2, description: "+5 damage, 2m blast. Catastrophic terminal effect." } + } + } +}; + +// Weapon type → default caliber mapping (matches weaponType field) +// Used as fallback only — explicit caliber on the item always takes priority. +const SLA_WEAPON_DEFAULT_CALIBER = { + "Revolver": ".44", + "Pistol": "10mm", + "Machine Pistol": "3mm", + "SMG": "9mm", + "Carbine": "7mm", + "Rifle": "5.56mm", + "Assault Rifle": "5.56mm", + "Battle Rifle": "7.62mm", + "Sniper": "7.62mm", + "Sniper Rifle": "8mm", + "LMG": "7mm", + "HW": "12.7mm", + "Heavy Weapon": "12.7mm", + "Autocannon": "12.7mm", + "Shotgun": "12g", + "Launcher": "40mm", + "Grenade": "40mm", + "Rail Gun": "Rail" +}; + +/** + * Get the cost per round for a weapon's current round type. + * Reads `caliber` and `roundType` from weaponData. + * Falls back to default caliber for weaponType if caliber not set. + */ +function _getAmmoCostForWeapon(weaponData) { + const caliber = weaponData.caliber + || SLA_WEAPON_DEFAULT_CALIBER[weaponData.weaponType] + || null; + if (!caliber) return 0; + const catalog = SLA_AMMO_CATALOG[caliber]; + if (!catalog) return 0; + const roundType = (weaponData.roundType || weaponData.ammoType || "standard").toLowerCase(); + const rd = catalog.rounds[roundType] || catalog.rounds.standard; + return rd ? rd.costPerRound : 0; +} + +/** + * Get AP and damage modifiers from the current round type. + */ +function _getAmmoModifiers(weaponData) { + const caliber = weaponData.caliber + || SLA_WEAPON_DEFAULT_CALIBER[weaponData.weaponType] + || null; + if (!caliber) return { apBonus: 0, damageMod: 0, blastRadius: 0 }; + const catalog = SLA_AMMO_CATALOG[caliber]; + if (!catalog) return { apBonus: 0, damageMod: 0, blastRadius: 0 }; + const roundType = (weaponData.roundType || weaponData.ammoType || "standard").toLowerCase(); + const rd = catalog.rounds[roundType] || catalog.rounds.standard; + return rd ? { apBonus: rd.apBonus, damageMod: rd.damageMod, blastRadius: rd.blastRadius } : { apBonus: 0, damageMod: 0, blastRadius: 0 }; +} + +/** + * Deduct bullet tax from actor credits and update session ammo log. + * Bullet tax = 2× the base round cost (ammo purchase price + field levy). + * Called immediately when a weapon fires. + * Two separate update() calls so a finances schema issue never blocks the credit deduction. + */ +async function _applyBulletTax(actor, weaponData, roundsFired) { + if (!actor || roundsFired <= 0) return 0; + const costPerRound = _getAmmoCostForWeapon(weaponData); + if (costPerRound <= 0) return 0; + + // Bullet tax is DOUBLED: base cost (ammo) + tax of equal value = 2× + const totalCost = costPerRound * roundsFired * 2; + // Fall back to 500 (template default) not 0 — actors created before the + // credits field existed have undefined here, not zero. + const storedCredits = actor.system?.details?.credits; + const currentCredits = (storedCredits !== undefined && storedCredits !== null) + ? Number(storedCredits) : 500; + const newCredits = Math.max(0, currentCredits - totalCost); + + // ── Credit deduction — critical, standalone update ────────────────────── + try { + await actor.update({ "system.details.credits": newCredits }); + console.log(`SLA Zero Two | Bullet Tax: ${actor.name} credits ${currentCredits}¢ → ${newCredits}¢ (−${totalCost}¢)`); + } catch (err) { + console.error("SLA Zero Two | Bullet Tax credit deduction failed:", err); + return 0; + } + + // ── Finances tracking — non-critical, separate update ─────────────────── + try { + const sessionSpent = Number(actor.system?.finances?.ammoSpentSession ?? 0) + totalCost; + const bulletTaxRun = Number(actor.system?.finances?.expenses?.bulletTax ?? 0) + totalCost; + await actor.update({ + "system.finances.ammoSpentSession": sessionSpent, + "system.finances.expenses.bulletTax": bulletTaxRun + }); + } catch (_) { + // Finances tracking is optional — ignore failures silently + } + + return totalCost; +} + +/** + * Build a complete ammo stock for a weapon. + * Returns array of ammo item data objects (all 3 round types). + */ +function _buildAmmoStockForWeapon(weaponName, caliber) { + const catalog = SLA_AMMO_CATALOG[caliber]; + if (!catalog) return []; + const mag = catalog.magazineDefault; + return Object.entries(catalog.rounds).map(([type, rd]) => ({ + name: rd.name, + type: "ammo", + system: { + description: rd.description, + caliber, + roundType: type, + quantity: type === "standard" ? mag * 3 : mag, // 3 mags standard, 1 each for AP/HE + costPerRound: rd.costPerRound, + apBonus: rd.apBonus, + damageMod: rd.damageMod, + blastRadius: rd.blastRadius, + weight: 0 + } + })); +} + +// ── Hook: Auto-set caliber on weapons when created or updated ───────────────── +Hooks.on("createItem", async (item, _options, userId) => { + if (game.user?.id !== userId) return; + if (item.type !== "weapon") return; + const wt = item.system?.weaponType || ""; + if (!item.system?.caliber && SLA_WEAPON_DEFAULT_CALIBER[wt]) { + const caliber = SLA_WEAPON_DEFAULT_CALIBER[wt]; + const catalog = SLA_AMMO_CATALOG[caliber]; + if (!catalog) return; + await item.update({ + "system.caliber": caliber, + "system.roundType": "standard", + "system.roundCostStd": catalog.rounds.standard.costPerRound, + "system.roundCostAP": catalog.rounds.ap.costPerRound, + "system.roundCostHE": catalog.rounds.he.costPerRound + }); + } +}); + +// ── Chat helper: show bullet tax in weapon result messages ──────────────────── +function _formatBulletTaxLine(totalCost, roundsFired, costPerRound, roundType, caliber) { + if (!totalCost) return ''; + const label = caliber ? `${caliber} ${roundType}` : roundType; + return `
💸 Bullet Tax: ${roundsFired}× ${label} @ ${costPerRound}c = −${totalCost}c
`; +} + + +// ══════════════════════════════════════════════════════════════════════════════ +// ZERO ENGINE — CHARACTER SHEET MARK 2 +// ══════════════════════════════════════════════════════════════════════════════ + +class ZeroEngineActorSheetMk2 extends ZeroEngineActorSheet { + /** Override the base class's hardcoded template path */ + get template() { + return "systems/slazero2/templates/actor/character-sheet-mk2.hbs"; + } + + static get defaultOptions() { + return foundry.utils.mergeObject(super.defaultOptions, { + classes: ["slazero2", "sheet", "actor"], + template: "systems/slazero2/templates/actor/character-sheet-mk2.hbs", + width: 760, + height: 720, + tabs: [], + resizable: true + }); + } + + async getData(options = {}) { + const context = await super.getData(options); + + // ── Safe finances defaults ───────────────────────────────────────────── + if (!context.system.finances) { + context.system.finances = { + income: { salary: 0, bpnReward: 0, other: 0 }, + expenses: { accommodation: 0, drugs: 0, subscriptions: 0, other: 0, bulletTax: 0 }, + debt: 0, ammoSpentSession: 0 + }; + } + const inc = context.system.finances.income || {}; + const exp = context.system.finances.expenses || {}; + context.weeklyIncome = (inc.salary||0) + (inc.bpnReward||0) + (inc.other||0); + context.weeklyExpenses = (exp.accommodation||0) + (exp.drugs||0) + (exp.subscriptions||0) + (exp.other||0) + (exp.bulletTax||0); + context.weeklyNet = context.weeklyIncome - context.weeklyExpenses; + context.weeklyNetSign = context.weeklyNet >= 0 ? '+' : ''; + context.weeklyNetColor = context.weeklyNet >= 0 ? '#44cc66' : '#cc1111'; + + // ── Attribute groups — each attribute with its 3 skills underneath ─── + const ATTR_SKILL_MAP = { + strength: { label: 'STR', colorClass: 'str-color', skills: ['force','melee','stamina'] }, + agility: { label: 'AGI', colorClass: 'agi-color', skills: ['marksmanship','mobility','stealth'] }, + wits: { label: 'WIT', colorClass: 'wit-color', skills: ['crafting','observation','survival'] }, + empathy: { label: 'EMP', colorClass: 'emp-color', skills: ['healing','insight','persuasion'] } + }; + context.attributeGroups = Object.entries(ATTR_SKILL_MAP).map(([attrKey, cfg]) => { + const attrVal = context.system.attributes?.[attrKey]?.value || 2; + return { + key: attrKey, label: cfg.label, colorClass: cfg.colorClass, value: attrVal, + skills: cfg.skills.map(sk => { + const skill = context.system.skills?.[sk] || { value: 0, label: sk }; + return { + key: sk, label: skill.label || sk, value: skill.value || 0, + attribute: attrKey, + dots: Array.from({length: 5}, (_, i) => ({ filled: i < (skill.value || 0) })) + }; + }) + }; + }); + + // ── Flat skills list (for ebb skill separately) ─────────────────────── + const skillDotCount = 5; + context.skillsWithDots = Object.entries(context.system.skills || {}).map(([key, skill]) => ({ + key, label: skill.label || key, value: skill.value || 0, attribute: skill.attribute || '', + dots: Array.from({length: skillDotCount}, (_, i) => ({ filled: i < (skill.value || 0) })) + })); + context.attributeList = Object.entries(context.system.attributes || {}).map(([key, attr]) => ({ + key, value: attr.value || 2, label: key.toUpperCase().slice(0, 3) + })); + + // ── Weapon shot options for each weapon ──────────────────────────────── + context.weaponItems = (context.items || []).filter(i => i.type === 'weapon').map(w => { + const fireModes = Array.isArray(w.system?.fireModes) ? w.system.fireModes : ['single']; + const shots = _getShotOptions(fireModes); + const caliber = w.system?.caliber || SLA_WEAPON_DEFAULT_CALIBER?.[w.system?.weaponType] || ''; + const roundType = w.system?.roundType || 'standard'; + const costStd = w.system?.roundCostStd || 0; + const costAP = w.system?.roundCostAP || 0; + const costHE = w.system?.roundCostHE || 0; + const currentCost = roundType === 'ap' ? costAP : roundType === 'he' ? costHE : costStd; + return { ...w, caliber, roundType, currentCost, multiShot: shots.length > 1, shotOptions: shots }; + }); + + return context; + } + + activateListeners(html) { + super.activateListeners(html); + const root = html instanceof jQuery ? html[0] : html; + + // ── Tab switching (new template uses .mk2-nav-tab / .mk2-panel) ────────── + // Tab switching — template uses .mk2-tab, panels use data-panel matching data-tab + root.querySelectorAll('.mk2-tab').forEach(tab => { + tab.addEventListener('click', () => { + root.querySelectorAll('.mk2-tab').forEach(t => t.classList.remove('active')); + root.querySelectorAll('.mk2-panel').forEach(p => p.classList.remove('active')); + tab.classList.add('active'); + const panel = root.querySelector(`.mk2-panel[data-panel="${tab.dataset.tab}"]`); + if (panel) panel.classList.add('active'); + }); + }); + + // ── Round type selector ──────────────────────────────────────────────── + root.querySelectorAll('.round-select').forEach(btn => { + btn.addEventListener('click', async () => { + const itemId = btn.dataset.itemId; + const round = btn.dataset.round; + const item = this.actor.items.get(itemId); + if (!item) return; + await item.update({ "system.roundType": round }); + }); + }); + + // ── Mk2 critical buttons ─────────────────────────────────────────────── + root.querySelector('.physical-critical-btn')?.addEventListener('click', ev => { + ev.preventDefault(); this._rollPhysicalCritical(ev); + }); + root.querySelector('.mental-critical-btn')?.addEventListener('click', ev => { + ev.preventDefault(); this._rollMentalCritical(ev); + }); + + // ── Ammo quantity inline edit ────────────────────────────────────────── + root.querySelectorAll('.ammo-qty-input').forEach(input => { + input.addEventListener('change', async () => { + const itemId = input.dataset.itemId; + const item = this.actor.items.get(itemId); + if (!item) return; + await item.update({ "system.quantity": parseInt(input.value) || 0 }); + }); + }); + + // ── Net income live update ───────────────────────────────────────────── + const netEl = root.querySelector('#mk2-net-income'); + const updateNet = () => { + if (!netEl) return; + const salary = parseInt(root.querySelector('[name="system.finances.income.salary"]')?.value) || 0; + const bpn = parseInt(root.querySelector('[name="system.finances.income.bpnReward"]')?.value) || 0; + const other = parseInt(root.querySelector('[name="system.finances.income.other"]')?.value) || 0; + const acc = parseInt(root.querySelector('[name="system.finances.expenses.accommodation"]')?.value) || 0; + const drugs = parseInt(root.querySelector('[name="system.finances.expenses.drugs"]')?.value) || 0; + const subs = parseInt(root.querySelector('[name="system.finances.expenses.subscriptions"]')?.value) || 0; + const othExp = parseInt(root.querySelector('[name="system.finances.expenses.other"]')?.value) || 0; + const net = (salary + bpn + other) - (acc + drugs + subs + othExp); + netEl.textContent = `${net >= 0 ? '+' : ''}${net}c`; + netEl.style.color = net >= 0 ? '#44cc66' : '#cc1111'; + }; + root.querySelectorAll('[name^="system.finances"]').forEach(el => { + el.addEventListener('input', updateNet); + }); + updateNet(); + } +} + +// (Mk2 sheet registration moved to the main init hook above) + + +// ══════════════════════════════════════════════════════════════════════════════ +// SLA INDUSTRIES — COMBAT CONDITIONS + MULTI-SHOT SYSTEM +// ══════════════════════════════════════════════════════════════════════════════ + +// ─── CONDITION DEFINITIONS ─────────────────────────────────────────────────── + +const SLA_CONDITIONS = [ + { + id: "sla-bleeding", + label: "Bleeding", + icon: "icons/svg/blood.svg", + hpPerTurn: 1, + diceModifiers: {}, + description: "Lose 1 HP at the start of each turn. Cleared by any healing." + }, + { + id: "sla-stunned", + label: "Stunned", + icon: "icons/svg/daze.svg", + hpPerTurn: 0, + diceModifiers: { all: -3 }, + description: "-3 dice to all rolls. Clears at end of turn." + }, + { + id: "sla-pinned", + label: "Pinned", + icon: "icons/svg/target.svg", + hpPerTurn: 0, + diceModifiers: { melee: -2, marksmanship: -2, mobility: -2 }, + description: "-2 dice on attacks and mobility. Requires cover action to clear." + }, + { + id: "sla-suppressed", + label: "Suppressed", + icon: "icons/svg/cowled.svg", + hpPerTurn: 0, + diceModifiers: { all: -2 }, + description: "-2 dice to all actions. Cleared by passing a STAMINA roll." + }, + { + id: "sla-on-fire", + label: "On Fire", + icon: "icons/svg/fire.svg", + hpPerTurn: 2, + escalating: true, + diceModifiers: { all: -1 }, + description: "Lose 2+ HP/turn (escalates). -1 die to all rolls. Requires action to extinguish." + }, + { + id: "sla-blinded", + label: "Blinded", + icon: "icons/svg/blind.svg", + hpPerTurn: 0, + diceModifiers: { marksmanship: -3, melee: -2, observation: -3 }, + description: "-3 dice on ranged attacks and observation. -2 on melee." + }, + { + id: "sla-deafened", + label: "Deafened", + icon: "icons/svg/deaf.svg", + hpPerTurn: 0, + diceModifiers: { observation: -1 }, + description: "-1 die to observation. Cannot hear audio cues." + }, + { + id: "sla-broken", + label: "Broken", + icon: "icons/svg/skull.svg", + hpPerTurn: 0, + diceModifiers: { all: -2 }, + description: "-2 dice to all rolls. Must roll STAMINA to act. Cleared by healing to above 0 HP." + }, + { + id: "sla-winded", + label: "Winded", + icon: "icons/svg/daze.svg", + hpPerTurn: 0, + diceModifiers: { all: -1 }, + description: "Winded — -1 die to all rolls. Clears at end of round." + }, + { + id: "sla-broken-arm", + label: "Broken Arm", + icon: "icons/svg/blood.svg", + hpPerTurn: 0, + diceModifiers: { melee: -2, marksmanship: -1, force: -1 }, + description: "Broken arm — cannot use that arm; -2 Melee, -1 Marksmanship, -1 Force." + }, + { + id: "sla-broken-leg", + label: "Broken Leg", + icon: "icons/svg/blood.svg", + hpPerTurn: 0, + diceModifiers: { mobility: -2, stealth: -1 }, + description: "Broken leg — cannot run; -2 Mobility, -1 Stealth." + }, + { + id: "sla-concussed", + label: "Concussed", + icon: "icons/svg/daze.svg", + hpPerTurn: 0, + diceModifiers: { all: -1, observation: -1, wits: -1 }, + description: "Concussed — -1 die to all rolls, extra -1 to Observation/Wits-based rolls." + }, + { + id: "sla-gut-wound", + label: "Gut Wound", + icon: "icons/svg/blood.svg", + hpPerTurn: 0, + diceModifiers: { all: -2 }, + description: "Gut wound — severe pain, -2 dice to all rolls." + }, + { + id: "sla-panicking", + label: "Panicking", + icon: "icons/svg/terror.svg", + hpPerTurn: 0, + diceModifiers: { all: -2 }, + description: "Panicking — cannot act normally. -2 dice to all rolls." + }, + { + id: "sla-shaken", + label: "Shaken", + icon: "icons/svg/terror.svg", + hpPerTurn: 0, + diceModifiers: { all: -1 }, + description: "Shaken — rattled by horror or trauma. -1 die to all rolls." + } +]; + +// Conditions are registered in the main init hook (search for "SLA_CONDITIONS" above this line) + +// ─── CONDITION MODIFIER HELPER ──────────────────────────────────────────────── + +/** + * Play a short "ta-da" fanfare using the Web Audio API — no external files needed. + */ +function _playShowingOffFanfare() { + try { + const ctx = new (window.AudioContext || window.webkitAudioContext)(); + // Ascending major arpeggio: C5 E5 G5 C6 + const notes = [523.25, 659.25, 783.99, 1046.5]; + notes.forEach((freq, i) => { + const osc = ctx.createOscillator(); + const gain = ctx.createGain(); + osc.connect(gain); + gain.connect(ctx.destination); + osc.type = 'sine'; + osc.frequency.value = freq; + const t = ctx.currentTime + i * 0.13; + gain.gain.setValueAtTime(0, t); + gain.gain.linearRampToValueAtTime(0.35, t + 0.04); + gain.gain.exponentialRampToValueAtTime(0.001, t + 0.28); + osc.start(t); + osc.stop(t + 0.30); + }); + } catch(_) { /* WebAudio unavailable — silent fallback */ } +} + +/** + * Post a whispered GM report of every PC's current conditions, HP, Resolve, and Stress. + * Called hourly from the ready hook and on demand from the STATUS button on any sheet. + */ +async function _broadcastPCConditions() { + if (!game.user?.isGM) return; + + const pcs = (game.actors ?? []).filter(a => + a.type === 'character' && a.system?.details?.isPlayerCharacter === true + ); + + if (pcs.length === 0) { + ui.notifications.info('SLA Zero Two | No PCs found for condition report.'); + return; + } + + const condLabels = Object.fromEntries( + SLA_CONDITIONS.map(c => [c.id, { label: c.label, desc: c.description }]) + ); + + let rows = ''; + for (const pc of pcs) { + const hp = Number(pc.system.derivedStats?.health?.value ?? 0); + const hpMax = Number(pc.system.derivedStats?.health?.max ?? 0); + const res = Number(pc.system.derivedStats?.resolve?.value ?? 0); + const resMax = Number(pc.system.derivedStats?.resolve?.max ?? 0); + const stress = Number(pc.system.derivedStats?.stress ?? 0); + + const soTally = Number(pc.system.details?.showingOff ?? 0); + const hpColour = hp <= 0 ? '#ff2222' : hp <= hpMax * 0.3 ? '#ff8800' : '#44cc66'; + const resColour = res <= 0 ? '#ff2222' : res <= resMax * 0.3 ? '#ff8800' : '#44ccff'; + const stressColour = stress >= 8 ? '#ff2222' : stress >= 5 ? '#ff8800' : '#aaaaaa'; + const soColour = soTally >= 20 ? '#ff2222' : soTally >= 10 ? '#ff8800' : soTally > 0 ? '#ffcc00' : '#555'; + + const activeConds = [...(pc.statuses ?? new Set())] + .filter(id => condLabels[id]) + .map(id => `${condLabels[id].label} (${condLabels[id].desc})`) + .join(', '); + + rows += ` +
+
${pc.name}
+
+ ❤ HP ${hp}/${hpMax}   + 🧠 Resolve ${res}/${resMax}   + ⚡ Stress ${stress}   + ✨ Showing Off ${soTally} +
+
+ ${activeConds || 'No active conditions'} +
+
`; + } + + const now = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); + + await ChatMessage.create({ + speaker: { alias: '⚠ SLA Zero Two — PC Status Report' }, + content: `
+
Status Report — ${now} — ${pcs.length} PC${pcs.length !== 1 ? 's' : ''}
+ ${rows} +
`, + whisper: [game.user.id] + }); +} + +/** + * Apply a status condition to an actor reliably. + * Tries actor.toggleStatusEffect first (Foundry v14 API). If that fails or + * the condition still isn't in actor.statuses afterward, falls back to creating + * an ActiveEffect directly so conditions always land even if toggleStatusEffect + * is unavailable or throws. + * @param {Actor} actor + * @param {string} condId e.g. "sla-panicking" + * @param {string} [label] display name fallback + * @param {string} [icon] icon path fallback + */ +async function _applyStatusCondition(actor, condId, label, icon) { + // Skip if already active + if (actor.statuses?.has(condId)) return; + + // Try the standard API first + let applied = false; + try { + await actor.toggleStatusEffect(condId, { active: true }); + applied = actor.statuses?.has(condId) ?? false; + } catch(e) { + console.warn(`SLA Zero Two | toggleStatusEffect failed for "${condId}":`, e); + } + + if (!applied) { + // Fallback: look up from SLA_CONDITIONS or use provided label/icon + const condDef = SLA_CONDITIONS.find(c => c.id === condId); + const effectName = condDef?.label ?? label ?? condId; + const effectIcon = condDef?.icon ?? icon ?? 'icons/svg/aura.svg'; + try { + await actor.createEmbeddedDocuments('ActiveEffect', [{ + name: effectName, + img: effectIcon, + icon: effectIcon, + statuses: [condId], + disabled: false + }]); + console.log(`SLA Zero Two | Applied "${condId}" via fallback ActiveEffect on ${actor.name}`); + } catch(e2) { + console.error(`SLA Zero Two | Could not apply condition "${condId}" on ${actor.name}:`, e2); + } + } +} + +/** + * Returns dice-pool penalty from any active SLA conditions on this actor. + * Checks actor.statuses (Foundry v14 Set) and system.derivedStats.broken. + */ +function _getConditionModifiers(actor, { skill = "", attribute = "" } = {}) { + const statuses = actor.statuses ?? new Set(); + let bonus = 0; + const breakdown = []; + const sk = String(skill || "").toLowerCase(); + const at = String(attribute || "").toLowerCase(); + + // ── Built-in SLA conditions ───────────────────────────────────────────────── + // Bug fix (v1.7.6): "all" and skill/attribute-specific modifiers now STACK + // (e.g. Concussed = -1 all + -1 observation = -2 to Observation rolls). + // Attribute keys (e.g. "wits") are now respected too. + for (const cond of SLA_CONDITIONS) { + if (!statuses.has(cond.id)) continue; + const mods = cond.diceModifiers || {}; + if (mods.all) { + bonus += mods.all; + breakdown.push(`${cond.label} ${mods.all}`); + } + if (sk && mods[sk] !== undefined && mods[sk] !== null) { + bonus += mods[sk]; + breakdown.push(`${cond.label} (${sk}) ${mods[sk]}`); + } + if (at && mods[at] !== undefined && mods[at] !== null) { + bonus += mods[at]; + breakdown.push(`${cond.label} (${at}) ${mods[at]}`); + } + } + + // ── Active Effects: read custom dice bonus changes ────────────────────────── + // Users can attach an ActiveEffect with a change like: + // key: "flags.slazero2.diceBonus.all" value: 2 (any roll) + // key: "flags.slazero2.diceBonus.skills.melee" value: -1 (melee only) + // key: "flags.slazero2.diceBonus.attributes.wits" value: 1 (wits only) + // Mode is ignored — value is always added. This lets the GM grant "+2 dice" + // buffs from arbitrary effects without editing code. + for (const effect of actor.effects ?? []) { + if (effect.disabled) continue; + const changes = effect.changes ?? []; + for (const ch of changes) { + const key = String(ch.key || "").toLowerCase(); + const val = Number(ch.value); + if (!Number.isFinite(val) || val === 0) continue; + if (!key.startsWith("flags.slazero2.dicebonus.")) continue; + const rest = key.substring("flags.slazero2.dicebonus.".length); + let applies = false; + if (rest === "all") applies = true; + else if (rest === `skills.${sk}` && sk) applies = true; + else if (rest === `attributes.${at}` && at) applies = true; + if (applies) { + bonus += val; + breakdown.push(`${effect.name || "Effect"} ${val > 0 ? "+" : ""}${val}`); + } + } + } + + // Legacy broken flag — only apply while CURRENT HP is actually 0 + // (The flag used to persist forever after healing, causing a permanent -2 penalty) + if (actor.system?.derivedStats?.broken && !statuses.has("sla-broken")) { + const currentHp = Number( + typeof actor.system.derivedStats.health === "object" + ? actor.system.derivedStats.health?.value + : actor.system.derivedStats.health + ) || 0; + if (currentHp <= 0) { + bonus -= 2; + breakdown.push("Broken −2"); + } + // If HP > 0 the broken flag is stale — ignore it (cleared on next recalc) + } + + return { bonus, breakdown }; +} + +// ─── AUTO-DAMAGE FROM CONDITIONS ────────────────────────────────────────────── +// v1.7.6: Per-round damage from Bleeding / On Fire no longer depends on the +// combatant being on their turn. Conditions tick every ROUND_TICK_MS for any +// actor in the world that has them, whether combat is active or not. The +// previous combatTurnChange hook only fired inside combat, so a bleeding PC +// who walked out of the encounter would magically stop bleeding. +// +// Behaviour: +// • Ticks while the game is UNPAUSED (pause the game to halt bleed/fire) +// • Runs on the active GM client only (single source of truth, no dupes) +// • Default cadence: one YZE round = 10 real seconds +// • Both PCs and NPCs are damaged so combat encounters still tick down + +const ROUND_TICK_MS = 10 * 1000; +let _zeConditionTickHandle = null; + +async function _tickConditionsForActor(actor) { + if (!actor || (actor.type !== "character" && actor.type !== "npc")) return; + const statuses = actor.statuses ?? new Set(); + const hasBleed = statuses.has("sla-bleeding"); + const hasFire = statuses.has("sla-on-fire"); + if (!hasBleed && !hasFire) return; + + const updates = {}; + const messages = []; + let hp = Number(actor.system?.derivedStats?.health?.value ?? 0); + + if (hasBleed) { + const newHp = Math.max(0, hp - 1); + if (newHp !== hp) { + messages.push(`🩸 ${actor.name} bleeds — ${hp} → ${newHp} HP`); + hp = newHp; + updates["system.derivedStats.health.value"] = newHp; + if (newHp <= 0) messages.push(`⚠ ${actor.name} is at 0 HP!`); + } + } + + if (hasFire) { + const fireLvl = Number(actor.getFlag("slazero2", "fireLevel") ?? 1); + const dmg = fireLvl + 1; + const newHp = Math.max(0, hp - dmg); + if (newHp !== hp) { + messages.push(`🔥 ${actor.name} burns (level ${fireLvl}) — ${hp} → ${newHp} HP`); + hp = newHp; + updates["system.derivedStats.health.value"] = newHp; + } + try { await actor.setFlag("slazero2", "fireLevel", Math.min(fireLvl + 1, 5)); } + catch(_) { /* token actor without persistent flags */ } + } + + if (Object.keys(updates).length > 0) { + try { await actor.update(updates); } + catch(e) { console.warn("SLA Zero Two | tick condition update failed:", e); } + } + if (messages.length > 0) { + ChatMessage.create({ + speaker: { alias: "SLA Conditions" }, + content: `
${messages.join('
')}
` + }); + } +} + +async function _tickAllConditions() { + // Only the active GM should run this — prevents N-GM duplicate damage + if (!game.user?.isGM) return; + const activeGM = game.users?.activeGM ?? game.users?.find(u => u.isGM && u.active); + if (activeGM && activeGM.id !== game.user.id) return; + if (game.paused) return; + + for (const actor of game.actors ?? []) { + await _tickConditionsForActor(actor); + } +} + +Hooks.once("ready", () => { + if (_zeConditionTickHandle) return; + _zeConditionTickHandle = setInterval(() => { + _tickAllConditions().catch(e => console.warn("SLA Zero Two | condition tick error:", e)); + }, ROUND_TICK_MS); + console.log(`SLA Zero Two | Condition tick started — ${ROUND_TICK_MS / 1000}s/round, runs while game unpaused`); +}); + +// Clear fire level when fire is extinguished +Hooks.on("updateActor", async (actor, changes) => { + if (!game.user?.isGM) return; + // If fire condition removed, reset fire level + const statuses = actor.statuses ?? new Set(); + if (!statuses.has("sla-on-fire")) { + const lvl = actor.getFlag("slazero2", "fireLevel"); + if (lvl) await actor.unsetFlag("slazero2", "fireLevel"); + } +}); + +// ─── CONDITION APPLICATION UI ───────────────────────────────────────────────── + +/** + * Add condition buttons to the token HUD. + * This supplements Foundry's built-in status icon system. + */ +Hooks.on("renderTokenHUD", (hud, html, data) => { + // Foundry's built-in status icons will handle the SLA conditions + // since we registered them in CONFIG.statusEffects. + // No additional code needed here — the token HUD will show them. +}); + +// ══════════════════════════════════════════════════════════════════════════════ +// MULTI-SHOT SYSTEM +// Allows firing multiple rounds for bonus dice at cost of ammo + bullet tax. +// Shot count capped by weapon fire mode capability. +// ══════════════════════════════════════════════════════════════════════════════ + +const SHOT_OPTIONS = [ + { shots: 1, label: "1 Shot (single)", diceBonus: 0, requiresMode: ["single","semi","burst","auto"] }, + { shots: 2, label: "2 Shots (+1 die)", diceBonus: 1, requiresMode: ["semi","burst","auto"] }, + { shots: 3, label: "3 Shots (+2 dice)", diceBonus: 2, requiresMode: ["burst","auto"] }, + { shots: 6, label: "6 Shots (+3 dice)", diceBonus: 3, requiresMode: ["auto"] }, +]; + +/** + * Get which shot options are available for a weapon based on its fire modes. + */ +function _getShotOptions(fireModes) { + const modes = (Array.isArray(fireModes) ? fireModes : [fireModes]).map(m => String(m).toLowerCase()); + const hasAuto = modes.some(m => m === "auto" || m === "full_auto" || m === "full"); + const hasBurst = modes.some(m => m === "burst"); + const hasSemi = modes.some(m => m === "semi" || m === "semi_auto"); + const hasSingle = modes.some(m => m === "single"); + + const caps = []; + if (hasSingle || !hasSemi && !hasBurst && !hasAuto) caps.push("single"); + if (hasSemi || hasBurst || hasAuto) caps.push("semi"); + if (hasBurst || hasAuto) caps.push("burst"); + if (hasAuto) caps.push("auto"); + + return SHOT_OPTIONS.filter(opt => + opt.requiresMode.some(req => caps.includes(req)) + ); +} + +// ══════════════════════════════════════════════════════════════════════════════ +// SLA INDUSTRIES — GAME PAUSED ANIMATION +// Animated SVG overlay: dual rotating rings, SLA logo, scan-line, flicker +// ══════════════════════════════════════════════════════════════════════════════ + +let _zePauseRefreshHandle = null; + +function _zeInjectPauseAnimation() { + document.body?.classList.add("ze-pause-active"); + if (document.querySelector('.ze-pause-root')) return; + + const html = ` +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + SLA + + + + + + + OPERATIVE STATUS + STAND BY + + + + SLA INDUSTRIES · MORT CITY GRID + + + + + +
+
`; + + document.body?.insertAdjacentHTML('beforeend', html); +} + +function _zeRemovePauseAnimation() { + document.querySelector('.ze-pause-root')?.remove(); + document.body?.classList.remove('ze-pause-active'); +} + +function _zeQueuePauseRefresh(delay = 0) { + window.clearTimeout(_zePauseRefreshHandle); + _zePauseRefreshHandle = window.setTimeout(() => { + if (!game?.paused) return; + _zeInjectPauseAnimation(); + }, delay); +} + +Hooks.once('ready', () => { + if (game.paused) { + _zeQueuePauseRefresh(0); + _zeQueuePauseRefresh(60); + _zeQueuePauseRefresh(180); + } +}); + +Hooks.on('pauseGame', (paused) => { + if (paused) { + requestAnimationFrame(() => requestAnimationFrame(() => _zeInjectPauseAnimation())); + _zeQueuePauseRefresh(60); + _zeQueuePauseRefresh(200); + } else { + _zeRemovePauseAnimation(); + } +}); diff --git a/styles/slazero2.css b/styles/slazero2.css new file mode 100644 index 0000000..9602513 --- /dev/null +++ b/styles/slazero2.css @@ -0,0 +1,6579 @@ +/* SLA Zero Two - SLA Industries Character Sheet */ + +/* ======================================== + SLA BORG FONTS + ======================================== */ +@font-face { + font-family: "SLA Borg Title"; + src: url("/systems/slazero2/fonts/SLABorg-Title.woff2") format("woff2"); + font-weight: normal; + font-style: normal; +} +@font-face { + font-family: "SLA Borg UI"; + src: url("/systems/slazero2/fonts/SLABorg-UI.woff2") format("woff2"); + font-weight: normal; + font-style: normal; +} +@font-face { + font-family: "SLA Borg Tech"; + src: url("/systems/slazero2/fonts/SLABorg-Tech.woff2") format("woff2"); + font-weight: normal; + font-style: normal; +} +@font-face { + font-family: "SLA Borg Book"; + src: url("/systems/slazero2/fonts/SLABorg-Book.woff2") format("woff2"); + font-weight: normal; + font-style: normal; +} + +.slazero2 { + font-family: "SLA Borg UI", "Rajdhani", "Oxanium", sans-serif; +} + +.slazero2 .window-content { + background: #1a1a1a; + color: #e0e0e0; + padding: 0; +} + +/* ======================================== + SHEET HEADER - Balanced Layout + ======================================== */ + +.slazero2 .sheet-header { + display: flex; + flex-direction: column; + padding: 0; + background: linear-gradient(135deg, #2c2c2c 0%, #1a1a1a 100%); + border-bottom: none; + gap: 0; +} + +.slazero2 .drug-alerts { + display: flex; + flex-direction: column; + gap: 6px; + padding: 8px 12px; + border-bottom: 1px solid #333; + background: rgba(0, 0, 0, 0.28); +} + +.slazero2 .drug-alert { + border: 1px solid #555; + border-left: 4px solid #777; + border-radius: 5px; + padding: 6px 8px; + background: rgba(20, 20, 20, 0.7); + display: flex; + align-items: center; + gap: 8px; +} + +.slazero2 .drug-alert.active { + border-left-color: #00d46d; + background: rgba(0, 130, 60, 0.18); +} + +.slazero2 .drug-alert.withdrawal { + border-left-color: #ff7a00; + background: rgba(160, 70, 0, 0.22); +} + +.slazero2 .drug-alert-main { + display: flex; + gap: 8px; + align-items: center; +} + +.slazero2 .drug-alert-title { + font-weight: 700; + color: #fff; +} + +.slazero2 .drug-alert-duration { + font-size: 11px; + color: #ddd; +} + +.slazero2 .drug-alert-meta { + flex: 1; + font-size: 11px; + color: #c8c8c8; +} + +/* --- Top Row: Avatar + Name + SCL/Credits --- */ +.slazero2 .header-top-row { + display: flex; + align-items: center; + gap: 12px; + padding: 10px 12px 8px 12px; + border-bottom: 1px solid #333; +} + +.slazero2 .header-top-row .avatar { + flex: 0 0 56px; + height: 56px; +} + +.slazero2 .header-top-row .avatar .profile { + width: 100%; + height: 100%; + border: 2px solid #ff6600; + border-radius: 6px; + cursor: pointer; + object-fit: cover; + transition: border-color 0.2s; +} + +.slazero2 .header-top-row .avatar .profile:hover { + border-color: #ff8833; +} + +.slazero2 .header-top-info { + flex: 1; + display: flex; + flex-direction: column; + gap: 4px; +} + +.slazero2 .header-top-info .actor-name input { + width: 100%; + font-size: 20px; + font-weight: bold; + background: transparent; + border: none; + border-bottom: 2px solid #ff6600; + color: #fff; + padding: 2px 0; +} + +.slazero2 .header-top-stats { + display: flex; + gap: 12px; +} + +.slazero2 .mini-stat { + display: flex; + align-items: center; + gap: 6px; +} + +.slazero2 .mini-stat-label { + font-size: 11px; + font-weight: bold; + color: #ff6600; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.slazero2 .mini-stat input { + width: 60px; + text-align: center; + font-size: 13px; + font-weight: bold; + background: rgba(0, 0, 0, 0.3); + border: 1px solid #555; + border-radius: 3px; + padding: 3px 4px; + color: #fff; +} + +.slazero2 .mini-stat input:focus { + border-color: #ff6600; + outline: none; +} + +/* --- Details Row: Race, Training, Archetype --- */ +.slazero2 .header-details-row { + display: grid; + grid-template-columns: 1fr 1fr 1fr 150px; + gap: 6px; + padding: 6px 12px; + border-bottom: 1px solid #333; +} + +.slazero2 .header-details-row .detail-field { + display: flex; + flex-direction: column; + gap: 2px; +} + +.slazero2 .header-details-row .detail-field label { + font-size: 9px; + font-weight: bold; + color: #ff6600; + text-transform: uppercase; + letter-spacing: 0.4px; +} + +.slazero2 .header-details-row .detail-field select, +.slazero2 .header-details-row .detail-field input[type="text"] { + width: 100%; + background: rgba(0, 0, 0, 0.3); + border: 1px solid #444; + border-radius: 3px; + padding: 3px 5px; + color: #e0e0e0; + font-size: 11px; + height: 30px; +} + +.slazero2 .header-details-row .detail-field select:focus, +.slazero2 .header-details-row .detail-field input[type="text"]:focus { + outline: none; + border-color: #ff6600; +} + +.slazero2 .header-details-row .armor-roll-field { + justify-content: flex-end; +} + +/* --- Vitals Row: Health, Resolve, Stress, Ebb --- */ +.slazero2 .header-vitals-row { + display: grid; + grid-template-columns: 1fr 1fr 1fr 0.7fr; + gap: 8px; + padding: 8px 12px; + border-bottom: 1px solid #333; +} + +.slazero2 .vital-box { + background: rgba(255, 102, 0, 0.08); + border: 2px solid #ff6600; + border-radius: 5px; + padding: 6px 8px; + text-align: center; +} + +.slazero2 .vital-box label { + display: block; + font-weight: bold; + color: #ff6600; + font-size: 11px; + margin-bottom: 4px; + text-transform: uppercase; + letter-spacing: 0.8px; +} + +.slazero2 .vital-save-label { + cursor: pointer; + transition: opacity 0.15s; +} +.slazero2 .vital-save-label:hover { + opacity: 0.75; + text-decoration: underline dotted; +} + +.slazero2 .vital-values { + display: flex; + align-items: center; + justify-content: center; + gap: 4px; +} + +.slazero2 .vital-values input { + width: 40px; + text-align: center; + font-size: 18px; + font-weight: bold; + background: rgba(0, 0, 0, 0.3); + border: 1px solid #ff6600; + border-radius: 3px; + padding: 4px; + color: #fff; +} + +.slazero2 .vital-separator { + font-size: 16px; + color: #ff6600; + font-weight: bold; +} + +/* Health box */ +.slazero2 .health-box { + border-color: #cc3333; + background: rgba(204, 51, 51, 0.08); +} + +.slazero2 .health-box label { + color: #ff4444; +} + +.slazero2 .health-box .vital-values input { + border-color: #cc3333; +} + +.slazero2 .health-box .vital-separator { + color: #cc3333; +} + +/* Resolve box */ +.slazero2 .resolve-box { + border-color: #3388cc; + background: rgba(51, 136, 204, 0.08); +} + +.slazero2 .resolve-box label { + color: #55aaff; +} + +.slazero2 .resolve-box .vital-values input { + border-color: #3388cc; +} + +.slazero2 .resolve-box .vital-separator { + color: #3388cc; +} + +/* Stress box with Panic button */ +.slazero2 .stress-vital { + border-color: #8b0000; + background: rgba(139, 0, 0, 0.12); +} + +.slazero2 .stress-vital label { + color: #ff4444; +} + +.slazero2 .stress-group { + display: flex; + align-items: center; + justify-content: center; + gap: 6px; +} + +.slazero2 .stress-value { + width: 44px; + text-align: center; + font-size: 18px; + font-weight: bold; + background: rgba(139, 0, 0, 0.2); + border: 1px solid #8b0000; + border-radius: 3px; + padding: 4px; + color: #ff4444; +} + +/* REST box — standalone vital next to Stress */ +.slazero2 .rest-vital { + border-color: #007744; + background: rgba(0, 100, 60, 0.1); + display: flex; + align-items: center; + justify-content: center; +} + +.slazero2 .rest-vital .rest-btn { + /* Override the general rest-btn defaults — fill the box */ + width: 100%; + height: 100%; + padding: 8px 12px; + font-size: 12px; +} + +.slazero2 .manual-panic-button { + background: linear-gradient(135deg, #8b0000 0%, #5c0000 100%); + border: 1px solid #ff4444; + color: #fff; + font-weight: bold; + font-size: 10px; + padding: 5px 8px; + border-radius: 3px; + cursor: pointer; + transition: all 0.2s; + text-transform: uppercase; + letter-spacing: 0.3px; + white-space: nowrap; +} + +.slazero2 .manual-panic-button:hover { + background: linear-gradient(135deg, #a00000 0%, #700000 100%); + transform: translateY(-1px); + box-shadow: 0 3px 8px rgba(255, 68, 68, 0.4); +} + +.slazero2 .manual-panic-button i { + margin-right: 3px; +} + +.slazero2 .manual-armor-button { + background: linear-gradient(135deg, #222 0%, #141414 100%); + border: 1px solid #cfa35a; + color: #e8d3a9; + font-weight: bold; + font-size: 10px; + padding: 5px 8px; + border-radius: 3px; + cursor: pointer; + transition: all 0.2s; + text-transform: uppercase; + letter-spacing: 0.3px; + white-space: nowrap; +} + +.slazero2 .manual-armor-button:hover { + background: linear-gradient(135deg, #2b2b2b 0%, #1c1c1c 100%); + border-color: #ffd089; + color: #fff2cf; + transform: translateY(-1px); + box-shadow: 0 3px 8px rgba(255, 208, 137, 0.25); +} + +.slazero2 .manual-armor-button i { + margin-right: 3px; +} + +.slazero2 .header-armor-roll-btn { + width: 100%; + height: 30px; + font-size: 10px; + padding: 0 6px; +} + +/* Ebb box */ +.slazero2 .ebb-vital { + border-color: #0096ff; + background: rgba(0, 150, 255, 0.08); +} + +.slazero2 .ebb-vital label { + color: #0096ff; +} + +.slazero2 .ebb-vital input { + width: 100%; + text-align: center; + font-size: 18px; + font-weight: bold; + background: rgba(0, 0, 0, 0.3); + border: 1px solid #0096ff; + border-radius: 3px; + padding: 4px; + color: #0096ff; +} + +/* --- Critical Injuries Row --- */ +.slazero2 .header-crits-row { + padding: 8px 12px 10px 12px; +} + +.slazero2 .crit-buttons { + display: flex; + gap: 8px; + margin-bottom: 8px; +} + +.slazero2 .roll-physical-crit-btn, +.slazero2 .roll-mental-crit-btn { + flex: 1; + background: linear-gradient(135deg, #8b0000 0%, #5c0000 100%); + border: 2px solid #ff4444; + color: #fff; + font-weight: bold; + font-size: 11px; + padding: 6px 10px; + border-radius: 4px; + cursor: pointer; + transition: all 0.2s; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.slazero2 .roll-physical-crit-btn:hover { + background: linear-gradient(135deg, #a00000 0%, #700000 100%); + transform: translateY(-1px); + box-shadow: 0 3px 10px rgba(255, 68, 68, 0.4); +} + +.slazero2 .roll-mental-crit-btn { + background: linear-gradient(135deg, #4b0082 0%, #2e0052 100%); + border-color: #8b5cf6; +} + +.slazero2 .roll-mental-crit-btn:hover { + background: linear-gradient(135deg, #6a0dad 0%, #4b0082 100%); + transform: translateY(-1px); + box-shadow: 0 3px 10px rgba(139, 92, 246, 0.4); +} + +/* Active injuries */ +.slazero2 .active-injuries { + background: rgba(0, 0, 0, 0.25); + border-radius: 4px; + padding: 8px; + margin-top: 6px; +} + +.slazero2 .active-injuries h4 { + color: #ff6600; + font-size: 12px; + font-weight: bold; + text-transform: uppercase; + letter-spacing: 0.5px; + margin: 0 0 6px 0; + padding-bottom: 4px; + border-bottom: 1px solid rgba(255, 102, 0, 0.3); +} + +.slazero2 .mental-injuries h4 { + color: #8b5cf6; + border-bottom-color: rgba(139, 92, 246, 0.3); +} + +.slazero2 .injury-item { + background: rgba(255, 102, 0, 0.08); + border-left: 3px solid #ff6600; + padding: 6px 8px; + margin: 4px 0; + border-radius: 3px; +} + +.slazero2 .mental-injuries .injury-item { + background: rgba(139, 92, 246, 0.08); + border-left-color: #8b5cf6; +} + +.slazero2 .injury-item.lethal-injury { + background: rgba(255, 0, 0, 0.15); + border-left-color: #ff0000; + box-shadow: 0 0 8px rgba(255, 0, 0, 0.2); + animation: pulse-injury 3s infinite; +} + +@keyframes pulse-injury { + 0%, 100% { box-shadow: 0 0 8px rgba(255, 0, 0, 0.2); } + 50% { box-shadow: 0 0 16px rgba(255, 0, 0, 0.5); } +} + +.slazero2 .injury-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 6px; + margin-bottom: 2px; +} + +.slazero2 .injury-item strong { + font-size: 12px; + color: #fff; + font-weight: bold; +} + +.slazero2 .lethal-tag { + color: #ff0000; + font-weight: bold; + font-size: 10px; + animation: pulse-lethal 2s infinite; +} + +@keyframes pulse-lethal { + 0%, 100% { opacity: 1; text-shadow: 0 0 6px rgba(255, 0, 0, 0.8); } + 50% { opacity: 0.6; text-shadow: 0 0 3px rgba(255, 0, 0, 0.4); } +} + +.slazero2 .injury-effects { + font-size: 11px; + color: #ccc; + margin: 2px 0 0 0; + line-height: 1.3; +} + +.slazero2 .delete-injury-btn { + background: rgba(255, 0, 0, 0.15); + border: 1px solid #ff0000; + color: #ff6666; + cursor: pointer; + width: 20px; + height: 20px; + min-width: 20px; + padding: 0; + border-radius: 3px; + font-size: 10px; + transition: all 0.2s; + flex-shrink: 0; + flex-grow: 0; + display: flex; + align-items: center; + justify-content: center; + line-height: 1; +} + +.slazero2 .delete-injury-btn:hover { + background: rgba(255, 0, 0, 0.35); + color: #fff; + transform: scale(1.1); +} + +/* ======================================== + SHEET TABS + ======================================== */ + +.slazero2 .sheet-tabs { + display: flex; + border-bottom: 3px solid #ff6600; + background: #2c2c2c; + gap: 0; + padding: 0; + margin: 0; +} + +.slazero2 .sheet-tabs .item { + flex: 1; + padding: 6px 10px; + text-align: center; + background: rgba(0, 0, 0, 0.3); + color: #888; + font-weight: bold; + font-size: 12px; + text-transform: uppercase; + letter-spacing: 1px; + cursor: pointer; + transition: all 0.2s; + border: none; + border-bottom: 2px solid transparent; +} + +.slazero2 .sheet-tabs .item:hover { + background: rgba(255, 102, 0, 0.1); + color: #ff8833; +} + +.slazero2 .sheet-tabs .item.active { + background: rgba(255, 102, 0, 0.2); + color: #ff6600; + border-bottom-color: #ff6600; +} + +/* ======================================== + SHEET BODY + ======================================== */ + +.slazero2 .sheet-body { + padding: 8px 12px 12px 12px; + overflow-y: auto; + max-height: calc(100% - 135px); +} + +.slazero2 .sheet-body .tab { + display: none; +} + +.slazero2 .sheet-body .tab.active { + display: block; +} + +/* Section Headers */ +.slazero2 .section-header { + margin: 0 0 6px 0; + padding: 6px 10px; + background: linear-gradient(90deg, #ff6600 0%, #cc5200 100%); + color: #fff; + font-size: 14px; + font-weight: bold; + text-transform: uppercase; + letter-spacing: 2px; + border-radius: 4px; + text-align: center; +} + +/* ======================================== + ATTRIBUTES & SKILLS TABLE + ======================================== */ + +.slazero2 .attr-skill-table { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 6px; + background: rgba(255, 255, 255, 0.02); + padding: 6px; + border: 2px solid #333; + border-radius: 6px; +} + +.slazero2 .attr-column { + background: rgba(255, 102, 0, 0.05); + border: 1px solid #444; + border-radius: 4px; + padding: 6px; + display: flex; + flex-direction: column; + gap: 4px; +} + +/* Attribute Header */ +.slazero2 .attr-header { + background: rgba(255, 102, 0, 0.15); + border: 2px solid #ff6600; + border-radius: 4px; + padding: 8px 6px; + text-align: center; + cursor: pointer; + transition: all 0.2s; +} + +.slazero2 .attr-header:hover { + background: rgba(255, 102, 0, 0.25); + transform: translateY(-1px); + box-shadow: 0 2px 8px rgba(255, 102, 0, 0.3); +} + +.slazero2 .attr-name { + font-weight: bold; + color: #ff6600; + font-size: 13px; + text-transform: uppercase; + letter-spacing: 1px; + margin-bottom: 6px; +} + +.slazero2 .attr-value-row { + display: flex; + align-items: center; + justify-content: center; + gap: 6px; + flex-wrap: wrap; +} + +.slazero2 .attr-value { + font-size: 32px; + font-weight: bold; + color: #ff6600; + text-shadow: 0 0 8px rgba(255, 102, 0, 0.5); + min-width: 40px; + text-align: center; +} + +.slazero2 .attr-btn { + width: 16px; + height: 16px; + background: rgba(255, 102, 0, 0.15); + border: 1px solid #ff6600; + border-radius: 2px; + color: #ff6600; + font-size: 11px; + font-weight: bold; + cursor: pointer; + transition: all 0.2s; + display: flex; + align-items: center; + justify-content: center; + padding: 0; + line-height: 1; +} + +.slazero2 .attr-btn:hover { + background: #ff6600; + color: #000; + transform: scale(1.1); +} + +.slazero2 .attr-btn:active { + transform: scale(0.95); +} + +.slazero2 .attr-header input { + display: none; +} + +.slazero2 .attr-roll-btn { + flex-shrink: 0; + width: 24px; + height: 24px; + background: rgba(0, 150, 255, 0.2); + border: 2px solid #0096ff; + border-radius: 4px; + color: #0096ff; + font-size: 12px; + cursor: pointer; + transition: all 0.2s; + display: flex; + align-items: center; + justify-content: center; + padding: 0; + line-height: 1; +} + +.slazero2 .attr-roll-btn:hover { + background: #0096ff; + color: #000; + transform: scale(1.15); + box-shadow: 0 0 10px rgba(0, 150, 255, 0.6); +} + +.slazero2 .attr-roll-btn:active { + transform: scale(0.95); +} + +/* Skill Rows */ +.slazero2 .attr-column .skill-row { + display: flex; + align-items: center; + justify-content: space-between; + padding: 4px 6px; + background: rgba(0, 0, 0, 0.2); + border: 1px solid #333; + border-radius: 3px; + transition: background 0.2s; +} + +.slazero2 .attr-column .skill-row:hover { + background: rgba(255, 102, 0, 0.1); + border-color: #ff6600; +} + +.slazero2 .attr-column .skill-row label { + flex: 1; + color: #ccc; + font-weight: 500; + font-size: 12px; + pointer-events: none; +} + +.slazero2 .skill-controls { + display: flex; + align-items: center; + gap: 4px; +} + +.slazero2 .skill-value { + font-size: 16px; + font-weight: bold; + color: #ff6600; + min-width: 20px; + text-align: center; + padding: 2px 6px; + background: rgba(255, 102, 0, 0.15); + border-radius: 3px; +} + +.slazero2 .skill-increment, +.slazero2 .skill-decrement { + width: 14px; + height: 14px; + background: rgba(255, 102, 0, 0.15); + border: 1px solid #ff6600; + border-radius: 2px; + color: #ff6600; + font-size: 10px; + font-weight: bold; + cursor: pointer; + transition: all 0.2s; + display: flex; + align-items: center; + justify-content: center; + padding: 0; + line-height: 1; +} + +.slazero2 .skill-increment:hover, +.slazero2 .skill-decrement:hover { + background: #ff6600; + color: #000; + transform: scale(1.15); +} + +.slazero2 .skill-increment:active, +.slazero2 .skill-decrement:active { + transform: scale(0.9); +} + +.slazero2 .skill-roll { + width: 22px; + height: 22px; + background: rgba(0, 212, 255, 0.2); + border: 2px solid #00d4ff; + border-radius: 3px; + color: #00d4ff; + font-size: 14px; + cursor: pointer; + transition: all 0.2s; + display: flex; + align-items: center; + justify-content: center; + padding: 0; + line-height: 1; +} + +.slazero2 .skill-roll:hover { + background: #00d4ff; + color: #000; + transform: scale(1.15); + box-shadow: 0 0 10px rgba(0, 212, 255, 0.5); +} + +.slazero2 .skill-roll:active { + transform: scale(0.95); +} + +.slazero2 .skill-row input { + display: none; +} + +/* ======================================== + SPECIALTIES, EQUIPMENT, BIOGRAPHY + ======================================== */ + +.slazero2 .specialties-section { + margin-top: 20px; + padding: 15px; + background: rgba(255, 102, 0, 0.05); + border: 2px solid #ff6600; + border-radius: 8px; +} + +.slazero2 .specialties-section .section-header { + margin: 0 0 15px 0; + padding: 10px 15px; + display: flex; + align-items: center; + gap: 10px; +} + +/* Specialty Items List on Actor Sheet */ +.slazero2 .specialty-list { + display: flex; + flex-direction: column; + gap: 6px; +} + +.slazero2 .specialty-item { + display: flex; + align-items: center; + gap: 10px; + background: rgba(0, 0, 0, 0.25); + border: 1px solid #444; + border-left: 3px solid #ff6600; + border-radius: 4px; + padding: 8px 10px; + transition: all 0.2s; +} + +.slazero2 .specialty-item:hover { + background: rgba(255, 102, 0, 0.1); + border-color: #ff6600; +} + +.slazero2 .specialty-img { + flex-shrink: 0; + border-radius: 4px; + border: 1px solid #555; +} + +.slazero2 .specialty-info { + flex: 1; + min-width: 0; +} + +.slazero2 .specialty-name-row { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 2px; + flex-wrap: wrap; +} + +.slazero2 .specialty-name { + font-weight: bold; + font-size: 13px; + color: #fff; + cursor: pointer; + flex: 1; + min-width: 0; +} + +.slazero2 .specialty-name:hover { + color: #ff6600; + text-decoration: underline; +} + +.slazero2 .specialty-package { + font-size: 10px; + color: #ff6600; + background: rgba(255, 102, 0, 0.15); + padding: 1px 6px; + border-radius: 3px; + text-transform: uppercase; + letter-spacing: 0.5px; + white-space: nowrap; +} + +.slazero2 .specialty-effects { + font-size: 11px; + color: #aaa; + line-height: 1.3; +} + +.slazero2 .specialty-empty { + text-align: center; + padding: 15px; + color: #666; + font-size: 12px; +} + +.slazero2 .delete-specialty-btn { + background: rgba(255, 0, 0, 0.15); + border: 1px solid #ff0000; + color: #ff6666; + cursor: pointer; + width: 20px; + height: 20px; + min-width: 20px; + padding: 0; + border-radius: 3px; + font-size: 10px; + transition: all 0.2s; + flex-shrink: 0; + flex-grow: 0; + display: flex; + align-items: center; + justify-content: center; + line-height: 1; + margin-left: auto; +} + +.slazero2 .delete-specialty-btn:hover { + background: rgba(255, 0, 0, 0.35); + color: #fff; + transform: scale(1.1); +} + +.slazero2 .specialties-section, +.slazero2 .equipment-section, +.slazero2 .biography-section, +.slazero2 .gear-section, +.slazero2 .bio-section, +.slazero2 .weapons-section { + margin-bottom: 20px; +} + +.slazero2 .specialties-section h2, +.slazero2 .equipment-section h2, +.slazero2 .biography-section h2, +.slazero2 .gear-section h2, +.slazero2 .bio-section h2, +.slazero2 .weapons-section h2 { + margin: 0 0 15px 0; + padding: 10px; + background: linear-gradient(90deg, #ff6600 0%, transparent 100%); + color: #fff; + font-size: 20px; + border-radius: 4px; +} + +.slazero2 .bio-section h3 { + margin: 15px 0 10px 0; + padding: 6px; + background: rgba(255, 102, 0, 0.2); + color: #ff6600; + font-size: 16px; + border-radius: 3px; +} + +.slazero2 .editor { + min-height: 150px; + background: rgba(255, 255, 255, 0.05); + border: 1px solid #333; + border-radius: 6px; + padding: 10px; + color: #e0e0e0; +} + +.slazero2 .editor textarea { + width: 100%; + min-height: 100px; + background: rgba(0, 0, 0, 0.3); + border: 1px solid #444; + border-radius: 4px; + padding: 8px; + color: #e0e0e0; + font-family: inherit; + font-size: 13px; + line-height: 1.5; + resize: vertical; +} + +.slazero2 .editor textarea:focus { + outline: none; + border-color: #ff6600; + box-shadow: 0 0 8px rgba(255, 102, 0, 0.3); +} + +.slazero2 .editor textarea::placeholder { + color: #666; + font-style: italic; +} + +/* ======================================== + FORM INPUTS + ======================================== */ + +.slazero2 input:focus { + outline: none; + border-color: #ff8833; + box-shadow: 0 0 8px rgba(255, 102, 0, 0.3); +} + +.slazero2 input::placeholder { + color: #666; +} + +/* ======================================== + SCROLLBAR + ======================================== */ + +.slazero2 .sheet-body::-webkit-scrollbar { + width: 8px; +} + +.slazero2 .sheet-body::-webkit-scrollbar-track { + background: #1a1a1a; +} + +.slazero2 .sheet-body::-webkit-scrollbar-thumb { + background: #ff6600; + border-radius: 4px; +} + +.slazero2 .sheet-body::-webkit-scrollbar-thumb:hover { + background: #ff8833; +} + +/* ======================================== + YZE DICE DIALOG + ======================================== */ + +.slazero2.dialog .yze-roll-dialog { + padding: 15px; + background: #1a1a1a; +} + +.slazero2.dialog .yze-roll-dialog .form-group { + margin-bottom: 18px; +} + +.slazero2.dialog .yze-roll-dialog label { + display: block; + font-weight: bold; + color: #ff8833; + margin-bottom: 8px; + font-size: 15px; + text-transform: uppercase; + letter-spacing: 1px; +} + +.slazero2.dialog .modify-dice { + background: rgba(255, 102, 0, 0.2); + border: 2px solid #ff6600; + color: #ff6600; + padding: 10px; + cursor: pointer; + border-radius: 6px; + font-weight: bold; + font-size: 16px; + transition: all 0.2s; +} + +.slazero2.dialog .modify-dice:hover { + background: #ff6600; + color: #000; + transform: scale(1.05); + box-shadow: 0 0 10px rgba(255, 102, 0, 0.6); +} + +.slazero2.dialog input[type="number"] { + background: rgba(0, 0, 0, 0.5); + border: 2px solid #666; + color: #fff; + font-size: 18px; + font-weight: bold; + border-radius: 4px; +} + +.slazero2.dialog input[type="number"]:focus { + border-color: #ff6600; + outline: none; + box-shadow: 0 0 8px rgba(255, 102, 0, 0.4); +} + +.slazero2.dialog select { + background: rgba(0, 0, 0, 0.7); + border: 1px solid #555; + color: #e0e0e0; + border-radius: 4px; + padding: 5px 8px; + width: 100%; + font-size: 14px; + cursor: pointer; +} + +.slazero2.dialog select:focus { + border-color: #ff6600; + outline: none; + box-shadow: 0 0 6px rgba(255, 102, 0, 0.4); +} + +.slazero2.dialog select option { + background: #1a1a1a; + color: #e0e0e0; +} + +/* Dialog footer buttons (Roll / Cancel) */ +.slazero2.dialog .dialog-button, +.slazero2.dialog footer button { + background: rgba(40, 40, 40, 0.9); + border: 1px solid #555; + color: #e0e0e0; + border-radius: 4px; + padding: 6px 18px; + font-family: "SLA Borg UI", "Rajdhani", sans-serif; + font-weight: bold; + font-size: 14px; + cursor: pointer; + transition: all 0.2s; +} +.slazero2.dialog .dialog-button:hover, +.slazero2.dialog footer button:hover { + background: rgba(255, 102, 0, 0.25); + border-color: #ff6600; + color: #fff; +} +/* Primary action (Roll) */ +.slazero2.dialog .dialog-button.default, +.slazero2.dialog footer button.default, +.slazero2.dialog .dialog-button[data-button="roll"] { + background: rgba(255, 102, 0, 0.2); + border-color: #ff6600; + color: #ff8833; +} +.slazero2.dialog .dialog-button.default:hover, +.slazero2.dialog footer button.default:hover, +.slazero2.dialog .dialog-button[data-button="roll"]:hover { + background: #ff6600; + color: #000; + box-shadow: 0 0 10px rgba(255, 102, 0, 0.5); +} + +/* ======================================== + YZE ROLL RESULTS (Chat) + ======================================== */ + +.yze-roll-result { + padding: 8px 10px; + background: rgba(26, 26, 26, 0.95); + border: 2px solid #ff6600; + border-radius: 6px; + margin: 4px 0; +} + +.yze-dice-display { + display: flex; + flex-wrap: wrap; + gap: 5px; + justify-content: center; + margin-bottom: 6px; + padding: 6px; + background: rgba(0, 0, 0, 0.3); + border-radius: 5px; +} + +.yze-dice-display.yze-stress-bane { + box-shadow: 0 0 14px rgba(255, 45, 45, 0.45); + border: 1px solid rgba(255, 45, 45, 0.35); +} + +.yze-dice-divider { + flex-basis: 100%; + height: 1px; + background: linear-gradient(90deg, transparent, rgba(255, 45, 45, 0.6), transparent); + margin: 4px 0; +} + +.yze-stress-label { + flex-basis: 100%; + text-align: center; + font-size: 12px; + letter-spacing: 2px; + color: #ff2d2d; + font-weight: bold; + margin-bottom: 4px; +} + +.yze-die { + width: 34px; + height: 34px; + display: flex; + align-items: center; + justify-content: center; + font-size: 18px; + font-weight: bold; + background: #f4f4f4; + border: 2px solid #111; + border-radius: 5px; + color: #111; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); + position: relative; +} + +.yze-die.yze-success { + background: rgba(0, 212, 255, 0.3); + border-color: #00d4ff; + color: #00d4ff; + box-shadow: 0 0 10px rgba(0, 212, 255, 0.5); + font-size: 22px; +} + +.yze-die.yze-stress { + background: #0b0b0b; + border: 2px solid #ff2d2d; + color: #ff2d2d; + box-shadow: 0 0 10px rgba(255, 45, 45, 0.5); + position: relative; +} + +.yze-die.yze-stress::after { + content: "!"; + position: absolute; + top: -6px; + right: -6px; + width: 16px; + height: 16px; + line-height: 16px; + border-radius: 50%; + background: #ff2d2d; + color: #0b0b0b; + font-size: 11px; + font-weight: bold; + text-align: center; + box-shadow: 0 0 6px rgba(255, 45, 45, 0.7); +} + +.yze-die.yze-locked::before { + content: "🔒"; + position: absolute; + top: -6px; + left: -6px; + width: 16px; + height: 16px; + line-height: 16px; + border-radius: 50%; + background: #111; + color: #ffd6d6; + font-size: 11px; + font-weight: bold; + text-align: center; + box-shadow: 0 0 6px rgba(0, 0, 0, 0.6); +} + +.yze-die.yze-stress.yze-success { + background: #0b0b0b; + border-color: #00e5ff; + color: #00e5ff; + box-shadow: 0 0 12px rgba(0, 229, 255, 0.7); +} + +.yze-die.yze-stress.yze-bane { + background: #2a0000; + border-color: #ff2d2d; + color: #ff2d2d; + box-shadow: 0 0 14px rgba(255, 45, 45, 0.75); +} + +.yze-roll-result .yze-summary { + text-align: center; + font-size: 14px; + padding: 6px 10px; + background: rgba(255, 102, 0, 0.15); + border-radius: 5px; + border-top: 1px solid #ff6600; + color: #fff; +} + +.yze-roll-result .yze-summary strong { + color: #00d4ff; + font-size: 20px; + text-shadow: 0 0 6px rgba(0, 212, 255, 0.6); + border-bottom: 1px solid rgba(0, 212, 255, 0.5); + padding-bottom: 1px; +} + +.yze-roll-result .yze-summary .yze-success-label { + color: #bfefff; + border-bottom: 1px solid rgba(0, 212, 255, 0.35); + padding-bottom: 1px; +} + +.yze-roll-result .yze-summary .yze-banes { + color: #ff6b6b; + font-weight: bold; +} + +.yze-roll-result .yze-summary .yze-stress-banes { + color: #ff2d2d; + font-weight: bold; + font-size: 16px; + text-shadow: 0 0 6px rgba(255, 45, 45, 0.7); + border-bottom: 1px solid rgba(255, 45, 45, 0.5); + padding-bottom: 1px; +} + +.yze-roll-result .yze-summary .yze-stress-note { + color: #d0d0d0; + font-size: 11px; + margin-left: 4px; + font-style: italic; +} + +.yze-panic-badge { + text-align: center; + background: rgba(255, 45, 45, 0.2); + border: 1px solid rgba(255, 45, 45, 0.7); + color: #ff2d2d; + font-weight: bold; + font-size: 12px; + padding: 4px 8px; + border-radius: 4px; + letter-spacing: 1px; + margin-bottom: 8px; + text-transform: uppercase; +} + +.yze-stress-tag { + display: inline-block; + margin-left: 6px; + padding: 1px 6px; + border-radius: 4px; + background: rgba(255, 45, 45, 0.15); + border: 1px solid rgba(255, 45, 45, 0.4); + color: #ff2d2d; + font-weight: bold; + font-size: 11px; + letter-spacing: 0.5px; +} + +/* Push Mechanics */ +.yze-pushed-label { + text-align: center; + background: linear-gradient(90deg, transparent, rgba(255, 200, 0, 0.3), transparent); + color: #ffc800; + font-weight: bold; + font-size: 16px; + padding: 8px; + margin-bottom: 10px; + border-radius: 4px; + text-transform: uppercase; + letter-spacing: 2px; + text-shadow: 0 0 10px rgba(255, 200, 0, 0.5); +} + +.yze-push-section { + margin-top: 15px; + padding-top: 15px; + border-top: 2px solid rgba(255, 102, 0, 0.3); + text-align: center; +} + +.yze-push-button { + background: linear-gradient(135deg, #ffc800 0%, #ff8800 100%); + border: 2px solid #ffc800; + color: #000; + font-weight: bold; + font-size: 16px; + padding: 10px 20px; + border-radius: 6px; + cursor: pointer; + transition: all 0.2s; + text-transform: uppercase; + letter-spacing: 1px; + box-shadow: 0 4px 8px rgba(255, 200, 0, 0.3); + display: inline-flex; + flex-direction: column; + align-items: center; + gap: 4px; +} + +.yze-push-title { + font-size: 16px; + line-height: 1; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.4); +} + +.yze-push-rule { + font-size: 11px; + font-weight: 600; + text-transform: none; + letter-spacing: 0; + color: rgba(0, 0, 0, 0.7); +} + +.yze-push-button:hover { + background: linear-gradient(135deg, #ffdd00 0%, #ff9900 100%); + transform: translateY(-2px); + box-shadow: 0 6px 12px rgba(255, 200, 0, 0.5); +} + +.yze-push-button:active { + transform: translateY(0); + box-shadow: 0 2px 4px rgba(255, 200, 0, 0.3); +} + +.yze-push-note { + margin-top: 8px; + font-size: 12px; + color: #ffd6d6; + font-style: italic; + line-height: 1.35; +} + +/* ======================================== + PANIC & CRITICAL INJURY CHAT CARDS + ======================================== */ + +.panic-result { + background: #1c0808; + border: 2px solid #8b0000; + border-radius: 6px; + padding: 15px; + margin: 10px 0; + color: #e0e0e0; +} + +.panic-formula { + font-size: 14px; + color: #e0e0e0; + margin-bottom: 10px; +} + +.panic-total { + font-size: 20px; + color: #ff5555; + font-weight: bold; +} + +.panic-success { + background: #081c08; + border: 2px solid #00aa00; + border-radius: 4px; + padding: 12px; + color: #55ee55; + font-weight: bold; + text-align: center; + margin-top: 10px; + font-size: 16px; +} + +.panic-effect { + background: #111111; + border: 1px solid #ff4444; + border-radius: 4px; + padding: 12px; + margin-top: 10px; + color: #e0e0e0; +} + +.panic-effect strong { + color: #ff7777; + font-size: 16px; + display: block; + margin-bottom: 8px; + text-transform: uppercase; + letter-spacing: 1px; +} + +.critical-injury { + background: #1c080d; + border: 2px solid #dc143c; + border-radius: 6px; + padding: 15px; + margin: 10px 0; + color: #e0e0e0; +} + +.crit-roll { + font-size: 14px; + color: #e0e0e0; + margin-bottom: 10px; +} + +.crit-name { + color: #ff6b6b; + font-size: 18px; + font-weight: bold; + margin: 10px 0; + text-transform: uppercase; + letter-spacing: 1px; +} + +.lethal { + color: #ff0000; + font-weight: bold; + animation: pulse-lethal 2s infinite; + text-shadow: 0 0 8px rgba(255, 0, 0, 0.8); +} + +.time-limit { + background: #1a1200; + border: 1px solid #ffaa00; + border-radius: 4px; + padding: 8px; + color: #ffbb33; + font-weight: bold; + margin: 8px 0; +} + +.crit-effects, +.crit-healing, +.crit-recovery { + background: #111111; + border-radius: 4px; + padding: 8px; + margin: 8px 0; + color: #d8d8d8; +} + +.crit-effects strong, +.crit-healing strong { + color: #ff6600; + display: block; + margin-bottom: 4px; +} + +/* ======================================== + CHAT CARD — MISSING BACKGROUND FIX + All system chat output needs a solid dark background so that + coloured text remains readable regardless of Foundry's chat theme. + ======================================== */ + +/* Instability roll result */ +.instability-result { + background: #0e1a14; + border: 2px solid #ff6600; + border-radius: 6px; + padding: 10px 14px; + margin: 6px 0; + color: #e0e0e0; +} +.instability-result p { margin: 4px 0; color: #e0e0e0; } +.instability-result strong { color: #fff; } +.instability-no-effect { color: #55ee88 !important; font-weight: bold; } +.instability-effect-text { color: #d8d8d8 !important; font-size: 13px; line-height: 1.5; } +.instability-auto-note { + color: #ffcc55 !important; + font-size: 11px; + font-style: italic; + border-top: 1px solid rgba(255,204,85,0.3); + margin-top: 6px; + padding-top: 4px; +} + +/* Panic result — auto-note row */ +.panic-auto { + background: rgba(255, 200, 0, 0.1); + border: 1px solid rgba(255, 200, 0, 0.3); + border-radius: 3px; + padding: 4px 8px; + font-size: 12px; + color: #ffcc44; + margin-top: 6px; +} +.panic-auto strong { color: #ffe066; } + +/* Drug GM alert (stamina strain, drug state) */ +.drug-gm-alert { + background: #0d0d1a; + border: 1px solid rgba(0, 180, 255, 0.4); + border-radius: 5px; + padding: 8px 12px; + color: #c0d8ff; + font-size: 13px; +} +.drug-gm-alert strong { color: #88ccff; } + +/* Weapon reload notification */ +.weapon-reload-chat { + background: #0d1a0d; + border: 1px solid rgba(0, 200, 100, 0.35); + border-radius: 5px; + padding: 7px 10px; + color: #b8eec8; + font-size: 13px; +} + +/* ======================================== + ROLLABLE INDICATOR + ======================================== */ + +.slazero2 .rollable { + position: relative; +} + +.slazero2 .rollable::after { + content: '\1F3B2'; + position: absolute; + top: 5px; + right: 5px; + font-size: 12px; + opacity: 0; + transition: opacity 0.2s; +} + +.slazero2 .rollable:hover::after { + opacity: 0.6; +} + +/* ======================================== + ITEM SHEET STYLES + ======================================== */ + +.slazero2.sheet.item .window-content { + background: #1a1a1a; + color: #e0e0e0; + padding: 0; +} + +.slazero2.sheet.item .sheet-header { + background: linear-gradient(135deg, #2c2c2c 0%, #1a1a1a 100%); + padding: 10px 12px; + border-bottom: 2px solid #ff6600; +} + +.slazero2.sheet.item .item-header-row { + display: flex; + align-items: center; + gap: 12px; +} + +.slazero2.sheet.item .item-photo { + flex: 0 0 64px; + height: 64px; +} + +.slazero2.sheet.item .item-photo img { + width: 100%; + height: 100%; + border: 2px solid #ff6600; + border-radius: 6px; + cursor: pointer; + object-fit: cover; +} + +.slazero2.sheet.item .item-header-info { + flex: 1; +} + +.slazero2.sheet.item .item-name input { + width: 100%; + font-size: 20px; + font-weight: bold; + background: transparent; + border: none; + border-bottom: 2px solid #ff6600; + color: #fff; + padding: 4px 0; +} + +.slazero2.sheet.item .item-name input:focus { + outline: none; + border-bottom-color: #ff8833; +} + +.slazero2.sheet.item .item-type-label { + font-size: 11px; + color: #ff6600; + text-transform: uppercase; + letter-spacing: 1px; + margin-top: 4px; +} + +.slazero2.sheet.item .sheet-body { + padding: 12px; + overflow-y: auto; + max-height: calc(100% - 90px); +} + +/* Specialty Sheet Content */ +.slazero2 .specialty-sheet-content { + display: flex; + flex-direction: column; + gap: 12px; +} + +.slazero2 .specialty-meta { + display: flex; + flex-direction: column; + gap: 8px; + background: rgba(255, 102, 0, 0.05); + border: 1px solid #333; + border-radius: 6px; + padding: 10px; +} + +.slazero2 .specialty-meta-row { + display: flex; + align-items: center; + gap: 10px; +} + +.slazero2 .specialty-meta-row label { + flex: 0 0 140px; + font-size: 11px; + font-weight: bold; + color: #ff6600; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.slazero2 .specialty-meta-row select, +.slazero2 .specialty-meta-row input[type="text"] { + flex: 1; + background: rgba(0, 0, 0, 0.3); + border: 1px solid #444; + border-radius: 3px; + padding: 5px 8px; + color: #e0e0e0; + font-size: 12px; +} + +.slazero2 .specialty-meta-row select:focus, +.slazero2 .specialty-meta-row input[type="text"]:focus { + outline: none; + border-color: #ff6600; +} + +.slazero2 .specialty-description { + flex: 1; +} + +.slazero2 .specialty-description > label { + display: block; + font-size: 11px; + font-weight: bold; + color: #ff6600; + text-transform: uppercase; + letter-spacing: 0.5px; + margin-bottom: 6px; +} + +.slazero2 .specialty-description .editor { + min-height: 200px; +} + +/* Item Props (Weapon/Armor/Equipment) */ +.slazero2 .item-props { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)); + gap: 8px; + margin-bottom: 12px; + background: rgba(255, 102, 0, 0.05); + border: 1px solid #333; + border-radius: 6px; + padding: 10px; +} + +.slazero2 .item-prop { + display: flex; + flex-direction: column; + gap: 4px; +} + +.slazero2 .item-prop label { + font-size: 10px; + font-weight: bold; + color: #ff6600; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.slazero2 .item-prop input, +.slazero2 .item-prop select { + background: rgba(0, 0, 0, 0.3); + border: 1px solid #444; + border-radius: 3px; + padding: 5px 8px; + color: #e0e0e0; + font-size: 13px; + text-align: center; +} + +.slazero2 .item-prop input:focus, +.slazero2 .item-prop select:focus { + outline: none; + border-color: #ff6600; +} + +.slazero2 .item-description > label { + display: block; + font-size: 11px; + font-weight: bold; + color: #ff6600; + text-transform: uppercase; + letter-spacing: 0.5px; + margin-bottom: 6px; +} + +/* ======================================== + WEAPON LIST ON ACTOR SHEET + ======================================== */ + +.slazero2 .weapon-list { + display: flex; + flex-direction: column; + gap: 4px; +} + +.slazero2 .weapon-item { + display: flex; + align-items: center; + gap: 8px; + background: rgba(0, 0, 0, 0.25); + border: 1px solid #444; + border-left: 3px solid #ff6600; + border-radius: 4px; + padding: 6px 8px; + transition: all 0.2s; +} + +.slazero2 .weapon-item:hover { + background: rgba(255, 102, 0, 0.1); + border-color: #ff6600; +} + +.slazero2 .weapon-img { + flex-shrink: 0; + border-radius: 4px; + border: 1px solid #555; +} + +.slazero2 .weapon-info { + flex: 1; + min-width: 0; +} + +.slazero2 .weapon-name-row { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 2px; +} + +.slazero2 .weapon-name { + font-weight: bold; + font-size: 13px; + color: #fff; + cursor: pointer; +} + +.slazero2 .weapon-name:hover { + color: #ff6600; + text-decoration: underline; +} + +.slazero2 .weapon-type-badge { + font-size: 9px; + padding: 1px 6px; + border-radius: 3px; + text-transform: uppercase; + letter-spacing: 0.5px; + white-space: nowrap; + font-weight: bold; +} + +.slazero2 .weapon-type-badge.ranged { + color: #00d4ff; + background: rgba(0, 212, 255, 0.15); + border: 1px solid rgba(0, 212, 255, 0.3); +} + +.slazero2 .weapon-type-badge.melee { + color: #ff4444; + background: rgba(255, 68, 68, 0.15); + border: 1px solid rgba(255, 68, 68, 0.3); +} + +.slazero2 .weapon-stats-row { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.slazero2 .weapon-stat { + font-size: 11px; + color: #aaa; +} + +.slazero2 .weapon-stat strong { + color: #ff6600; + font-size: 10px; + margin-right: 2px; +} + +.slazero2 .weapon-actions { + display: flex; + flex-direction: row; + align-items: center; + gap: 6px; + flex-shrink: 0; +} + +.slazero2 .weapon-aux-actions { + display: flex; + flex-direction: column; + gap: 3px; +} + +.slazero2 .weapon-roll-btn { + height: auto; + min-width: 80px; + background: rgba(0, 212, 255, 0.18); + border: 2px solid #00d4ff; + border-radius: 5px; + color: #00d4ff; + font-size: 11px; + font-weight: bold; + letter-spacing: 0.5px; + text-transform: uppercase; + cursor: pointer; + transition: all 0.2s; + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + gap: 5px; + padding: 6px 10px; + white-space: nowrap; + overflow: hidden; + box-sizing: border-box; +} + +.slazero2 .weapon-roll-btn i { + font-size: 13px; + flex-shrink: 0; +} + +.slazero2 .weapon-roll-btn:hover { + background: #00d4ff; + color: #000; + box-shadow: 0 0 14px rgba(0, 212, 255, 0.55); +} + +.slazero2 .weapon-roll-btn:active { + transform: scale(0.95); +} + +.slazero2 .weapon-roll-btn.btn-locked, +.slazero2 .weapon-roll-btn:disabled { + background: rgba(80,80,80,0.2); + border-color: #555; + color: #555; + cursor: not-allowed; + box-shadow: none; +} + +.slazero2 .weapon-item.weapon-ammo-empty { + border-left-color: #cc4400; + background: rgba(80,20,0,0.2); + opacity: 0.8; +} + +.slazero2 .ammo-empty-badge { + font-size: 9px; + font-weight: bold; + text-transform: uppercase; + color: #ff4400; + background: rgba(200,40,0,0.2); + border: 1px solid #cc3300; + border-radius: 3px; + padding: 1px 5px; + letter-spacing: 0.5px; +} + +.slazero2 .toggle-equip-btn { + width: 22px; + height: 20px; + background: rgba(120, 120, 120, 0.18); + border: 1px solid #888; + border-radius: 3px; + color: #cfcfcf; + font-size: 11px; + cursor: pointer; + transition: all 0.2s; + display: flex; + align-items: center; + justify-content: center; + padding: 0; +} + +.slazero2 .toggle-equip-btn:hover { + border-color: #cfcfcf; + color: #fff; + transform: translateY(-1px); +} + +.slazero2 .toggle-equip-btn.equipped { + background: rgba(0, 170, 0, 0.22); + border-color: #29d429; + color: #a9ff9a; + box-shadow: 0 0 8px rgba(41, 212, 41, 0.25); +} + +.slazero2 .drug-activate-btn, +.slazero2 .drug-off-btn, +.slazero2 .drug-clear-withdrawal-btn { + width: 30px; + height: 30px; + border-radius: 4px; + border: 1px solid #888; + color: #eee; + background: rgba(70, 70, 70, 0.2); + cursor: pointer; + display: inline-flex; + align-items: center; + justify-content: center; + transition: all 0.2s; + padding: 0; +} + +.slazero2 .drug-alert .drug-off-btn { + width: auto; + min-width: 54px; + padding: 0 8px; + font-size: 11px; +} + +.slazero2 .drug-activate-btn { + border-color: #26c96b; + color: #9fffc4; + background: rgba(0, 130, 60, 0.2); +} + +.slazero2 .drug-off-btn { + border-color: #ff9f40; + color: #ffd6a9; + background: rgba(170, 80, 0, 0.22); +} + +.slazero2 .drug-clear-withdrawal-btn { + border-color: #9db4cc; + color: #d7e7ff; + background: rgba(40, 70, 110, 0.22); +} + +.slazero2 .drug-activate-btn:hover, +.slazero2 .drug-off-btn:hover, +.slazero2 .drug-clear-withdrawal-btn:hover { + transform: translateY(-1px); + filter: brightness(1.2); +} + +.slazero2 .drug-item.drug-active { + border-left-color: #00d46d; + background: rgba(0, 110, 55, 0.16); +} + +.slazero2 .drug-item.drug-withdrawal { + border-left-color: #ff7a00; + background: rgba(150, 75, 0, 0.2); +} + +.slazero2 .armor-roll-btn { + width: 32px; + height: 32px; + background: rgba(207, 163, 90, 0.2); + border: 2px solid #cfa35a; + border-radius: 4px; + color: #f5d6a2; + font-size: 15px; + cursor: pointer; + transition: all 0.2s; + display: flex; + align-items: center; + justify-content: center; + padding: 0; +} + +.slazero2 .armor-roll-btn:hover { + background: #cfa35a; + color: #111; + transform: scale(1.1); + box-shadow: 0 0 10px rgba(207, 163, 90, 0.45); +} + +.slazero2 .armor-roll-btn:active { + transform: scale(0.95); +} + +.slazero2 .delete-weapon-btn { + background: rgba(255, 0, 0, 0.15); + border: 1px solid #ff0000; + color: #ff6666; + cursor: pointer; + width: 20px; + height: 20px; + min-width: 20px; + padding: 0; + border-radius: 3px; + font-size: 10px; + transition: all 0.2s; + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + line-height: 1; +} + +.slazero2 .delete-weapon-btn:hover { + background: rgba(255, 0, 0, 0.35); + color: #fff; + transform: scale(1.1); +} + +.slazero2 .delete-armor-btn { + background: rgba(255, 0, 0, 0.15); + border: 1px solid #ff0000; + color: #ff6666; + cursor: pointer; + width: 20px; + height: 20px; + min-width: 20px; + padding: 0; + border-radius: 3px; + font-size: 10px; + transition: all 0.2s; + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; +} + +.slazero2 .delete-armor-btn:hover { + background: rgba(255, 0, 0, 0.35); + color: #fff; + transform: scale(1.1); +} + +.slazero2 .delete-gear-btn { + background: rgba(255, 0, 0, 0.15); + border: 1px solid #ff0000; + color: #ff6666; + cursor: pointer; + width: 20px; + height: 20px; + min-width: 20px; + padding: 0; + border-radius: 3px; + font-size: 10px; + transition: all 0.2s; + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; +} + +.slazero2 .delete-gear-btn:hover { + background: rgba(255, 0, 0, 0.35); + color: #fff; + transform: scale(1.1); +} + +.slazero2 .delete-drug-btn { + background: rgba(255, 0, 0, 0.15); + border: 1px solid #ff0000; + color: #ff6666; + cursor: pointer; + width: 20px; + height: 20px; + min-width: 20px; + padding: 0; + border-radius: 3px; + font-size: 10px; + transition: all 0.2s; + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; +} + +.slazero2 .delete-drug-btn:hover { + background: rgba(255, 0, 0, 0.35); + color: #fff; + transform: scale(1.1); +} + +.slazero2 .weapon-empty { + text-align: center; + padding: 12px; + color: #666; + font-size: 12px; +} + +/* REST button */ +.slazero2 .rest-btn { + background: rgba(0, 140, 90, 0.22); + border: 1px solid #00a860; + border-radius: 4px; + color: #55ffaa; + font-size: 11px; + font-weight: bold; + letter-spacing: 0.5px; + text-transform: uppercase; + padding: 3px 8px; + cursor: pointer; + transition: all 0.2s; + white-space: nowrap; +} + +.slazero2 .rest-btn:hover { + background: rgba(0, 170, 100, 0.35); + box-shadow: 0 0 8px rgba(0, 168, 96, 0.45); +} + +/* Finance amount wrapper keeps input + 'c' aligned */ +.slazero2 .fin-amount { + display: flex; + align-items: center; + gap: 4px; +} + +.slazero2 .fin-amount span { + color: #888; + font-size: 11px; +} + +/* Ammo empty indicator in chat cards */ +.ammo-chat-empty { + color: #ff4444 !important; + font-weight: bold; +} + +/* ======================================== + WEAPON DAMAGE IN CHAT CARDS + ======================================== */ + +.yze-weapon-damage { + margin-top: 12px; + background: rgba(255, 102, 0, 0.08); + border: 2px solid #ff6600; + border-radius: 6px; + overflow: hidden; +} + +.weapon-damage-header { + background: linear-gradient(90deg, #ff6600 0%, #cc5200 100%); + color: #fff; + font-weight: bold; + font-size: 14px; + padding: 6px 12px; + text-transform: uppercase; + letter-spacing: 1px; +} + +.weapon-damage-header i { + margin-right: 6px; +} + +.weapon-damage-stats { + padding: 10px 12px; +} + +.weapon-damage-total { + display: flex; + align-items: center; + justify-content: center; + gap: 10px; + margin-bottom: 4px; +} + +.weapon-damage-total .damage-label { + font-size: 12px; + color: #aaa; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.weapon-damage-total .damage-value { + font-size: 32px; + font-weight: bold; + color: #ff6600; + text-shadow: 0 0 10px rgba(255, 102, 0, 0.5); +} + +.weapon-damage-breakdown { + text-align: center; + font-size: 11px; + color: #888; + margin-bottom: 8px; +} + +.weapon-damage-miss { + text-align: center; + padding: 6px; +} + +.weapon-damage-miss span { + font-size: 20px; + font-weight: bold; + color: #ff4444; + text-transform: uppercase; + letter-spacing: 3px; +} + +.weapon-damage-info { + display: flex; + justify-content: center; + flex-wrap: wrap; + gap: 12px; + padding-top: 8px; + border-top: 1px solid rgba(255, 102, 0, 0.2); +} + +.weapon-damage-info .wdi { + font-size: 11px; + color: #ccc; +} + +.weapon-damage-info .wdi strong { + color: #ff6600; + margin-right: 3px; +} + + + + +/* Initiative chat breakdown */ +.yze-initiative-card { + border: 1px solid rgba(255, 179, 0, 0.4); + background: linear-gradient(180deg, #1e1a10 0%, #141208 100%); + padding: 8px 10px; + border-radius: 6px; + color: #d0c8a0; + font-size: 12px; + line-height: 1.5; +} + +.yze-initiative-title { + font-weight: 700; + text-transform: uppercase; + letter-spacing: 1px; + color: #ffb300; + font-size: 12px; + margin-bottom: 5px; + border-bottom: 1px solid rgba(255,179,0,0.25); + padding-bottom: 3px; +} + +.yze-initiative-formula { + color: #c8c0a0; + margin-bottom: 4px; +} + +.init-base { + color: #ff9933; + font-size: 14px; +} + +.yze-initiative-dice { + margin-bottom: 4px; +} + +.yze-initiative-successes .init-suc { + color: #00d4ff; + font-size: 14px; +} + +.yze-initiative-bonus { + color: #c8c0a0; +} +.init-bon { color: #88ddaa; font-size: 13px; } +.init-breakdown { font-size: 10px; color: #888; font-style: italic; } + +.yze-initiative-total { + margin-top: 5px; + padding-top: 5px; + border-top: 1px solid rgba(255,179,0,0.2); + font-size: 14px; +} +.init-final { + color: #ffb300; + font-size: 18px; + text-shadow: 0 0 8px rgba(255,179,0,0.5); +} + +.yze-initiative-tiebreak { + margin-top: 4px; + font-size: 11px; + color: #777; +} + + +/* Initiative buttons inline (next to name/SCL) */ +.slazero2 .init-buttons-inline { + display: flex; + gap: 6px; + align-items: center; + margin-left: auto; +} + +.slazero2 .init-buttons-inline button { + display: inline-flex !important; + width: auto !important; + min-width: 72px; + height: 24px; + align-items: center; + justify-content: center; + font-family: var(--font-gridnik), "Roboto", sans-serif; + font-weight: 700; + font-size: 10px; + letter-spacing: 0.8px; + padding: 3px 8px; + border-radius: 5px; + border: 1px solid var(--color-border--core-box-orange); + background: #2a2a2a; + color: #ffb300 !important; + text-transform: uppercase; + box-shadow: inset 0 0 0 1px #00000066; + line-height: 1; +} + +.slazero2 .init-buttons-inline button:hover { + background: #3a3a3a; + color: #ffd56a; +} + +.slazero2 .init-buttons-inline button:active { + background: #242424; +} + +.ze-combatant-init-controls { + display: inline-flex; + gap: 4px; + margin-left: 10px; + vertical-align: middle; + align-items: center; + flex-wrap: nowrap; +} + +.ze-combatant-init-controls .ze-init-score { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 28px; + height: 22px; + padding: 0 6px; + border: 1px solid #7a3f1a; + border-radius: 4px; + background: #161616; + color: #8fd7ff; + font-size: 11px; + font-weight: 700; +} + +.ze-combatant-init-controls .ze-row-init-btn { + min-width: 48px; + height: 22px; + padding: 0 7px; + border: 1px solid #7a3f1a; + border-radius: 4px; + background: #1f1f1f; + color: #ffb65a; + font-size: 11px; + font-weight: 700; + line-height: 1; + white-space: nowrap; +} + +.ze-combatant-init-controls .ze-row-init-btn:hover { + background: #2a2a2a; + color: #ffd08a; +} + +/* Hide yze-combat card-roll affordances; SLA Zero Two uses AGI/WIT initiative buttons. */ +.combat-sidebar li.combatant .token-initiative .roll { + display: none !important; +} + +.chat-message .initiative-draw { + display: none !important; +} + +/* ======================================== + SLA FONT APPLICATIONS + ======================================== */ + +/* Section headers & named labels: SLA Borg Title */ +.slazero2 .section-header, +.slazero2 h2, +.slazero2 h3, +.slazero2 h4, +.slazero2 .sheet-tab-nav .item, +.slazero2 .tab-label, +.slazero2 .drug-name, +.slazero2 .specialties-section h2, +.slazero2 .equipment-section h2, +.slazero2 .biography-section h2, +.slazero2 .gear-section h2, +.slazero2 .bio-section h2, +.slazero2 .weapons-section h2 { + font-family: "SLA Borg Title", "Orbitron", "Rajdhani", sans-serif; +} + +/* Technical / numeric values: SLA Borg Tech */ +.slazero2 .boxed-value, +.slazero2 .health-box, +.slazero2 .resolve-box, +.slazero2 .ze-attr-die, +.slazero2 .ze-skill-die, +.slazero2 .stress-value, +.slazero2 .scl-value, +.slazero2 .credits-value, +.slazero2 input[type="number"] { + font-family: "SLA Borg Tech", "Share Tech Mono", "Courier New", monospace; +} + +/* Actor name: SLA Borg Title, larger */ +.slazero2 .actor-name input { + font-family: "SLA Borg Title", "Orbitron", sans-serif; +} + +/* ======================================== + VERTICAL SKILLS LAYOUT (.ze-stats) + ======================================== */ + +.slazero2 .ze-stats { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 12px; + align-items: start; + padding: 12px 10px; +} + +/* One block per attribute */ +.slazero2 .ze-attribute-block { + background: rgba(255, 102, 0, 0.04); + border: 1px solid rgba(255, 102, 0, 0.25); + border-radius: 6px; + overflow: hidden; +} + +/* Attribute header row */ +.slazero2 .ze-attribute-header { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + background: linear-gradient(90deg, rgba(255, 102, 0, 0.22) 0%, rgba(30, 30, 30, 0.8) 100%); + border-bottom: 1px solid rgba(255, 102, 0, 0.3); +} + +.slazero2 .ze-attr-roll-btn { + display: flex; + align-items: center; + gap: 6px; + background: none; + border: none; + cursor: pointer; + padding: 0; + flex: 1; +} + +.slazero2 .ze-attr-name { + font-family: "SLA Borg Title", "Orbitron", sans-serif; + font-size: 15px; + font-weight: bold; + color: #ff8833; + text-transform: uppercase; + letter-spacing: 1.5px; +} + +.slazero2 .ze-attr-roll-btn i { + color: #ff6600; + font-size: 20px; + opacity: 0.85; + filter: drop-shadow(0 0 4px rgba(255, 102, 0, 0.5)); +} + +.slazero2 .ze-attr-roll-btn:hover .ze-attr-name { + color: #ffaa55; +} +.slazero2 .ze-attr-roll-btn:hover i { + opacity: 1; +} + +/* Score selector (dots/boxes) for attribute */ +.slazero2 .ze-attr-score { + display: flex; + align-items: center; +} + +/* Attribute die value badge */ +.slazero2 .ze-attr-die { + font-family: "SLA Borg Tech", "Share Tech Mono", monospace; + font-size: 13px; + font-weight: bold; + color: #ff8833; + background: rgba(0, 0, 0, 0.4); + border: 1px solid #ff6600; + border-radius: 4px; + padding: 2px 8px; + min-width: 36px; + text-align: center; + cursor: pointer; +} +.slazero2 .ze-attr-die:hover { + background: rgba(255, 102, 0, 0.2); + box-shadow: 0 0 6px rgba(255, 102, 0, 0.4); +} + +/* Skills list beneath each attribute */ +.slazero2 .ze-skills-list { + display: flex; + flex-direction: column; + gap: 0; +} + +/* Individual skill row */ +.slazero2 .ze-skill-row { + display: flex; + align-items: center; + gap: 8px; + padding: 6px 12px; + border-bottom: 1px solid rgba(255, 255, 255, 0.04); + transition: background 0.15s; +} + +.slazero2 .ze-skill-row:last-child { + border-bottom: none; +} + +.slazero2 .ze-skill-row:hover { + background: rgba(255, 102, 0, 0.08); +} + +/* Skill name link */ +.slazero2 .ze-skill-name { + font-family: "SLA Borg UI", "Rajdhani", sans-serif; + font-size: 14px; + color: #c8c8c8; + flex: 1; + cursor: pointer; + text-decoration: none; + letter-spacing: 0.5px; +} + +.slazero2 .ze-skill-name:hover { + color: #ff8833; + text-decoration: none; +} + +/* Score selector for skill */ +.slazero2 .ze-skill-score { + display: flex; + align-items: center; +} + +/* Skill die value badge */ +.slazero2 .ze-skill-die { + font-family: "SLA Borg Tech", "Share Tech Mono", monospace; + font-size: 12px; + font-weight: bold; + color: #999; + background: rgba(0, 0, 0, 0.3); + border: 1px solid #444; + border-radius: 3px; + padding: 2px 7px; + min-width: 32px; + text-align: center; + cursor: pointer; +} + +.slazero2 .ze-skill-die:hover { + color: #ff8833; + border-color: #ff6600; + background: rgba(255, 102, 0, 0.15); +} + +/* ======================================== + EBB & FLUX SYSTEM + ======================================== */ + +/* Flux vital box — cyan/teal to distinguish from health/resolve/stress */ +.slazero2 .flux-vital { + border-color: #00cccc !important; +} +.slazero2 .flux-vital label { + color: #00cccc !important; + font-family: "SLA Borg Title", "Orbitron", sans-serif; + letter-spacing: 1.5px; +} +.slazero2 .flux-vital .vital-values { + display: flex; + align-items: center; + gap: 4px; +} +.slazero2 .flux-vital .flux-current { + color: #00cccc !important; + border-color: #00cccc !important; + background: rgba(0, 204, 204, 0.08) !important; + width: 50px; + text-align: center; +} +.slazero2 .flux-vital .vital-separator { + color: #888; + font-weight: bold; +} +.slazero2 .flux-vital .flux-max-display { + color: #00cccc; + font-family: "SLA Borg Tech", "Share Tech Mono", monospace; + font-size: 18px; + font-weight: bold; + min-width: 24px; + text-align: center; +} + +/* Ebb skill row — highlighted in cyan */ +.slazero2 .ebb-skill-row { + background: rgba(0, 204, 204, 0.06); + border-top: 1px solid rgba(0, 204, 204, 0.3); + border-radius: 0 0 4px 4px; +} +.slazero2 .ebb-skill-row label { + color: #00cccc !important; + font-weight: bold; +} + +/* Ebb Formulae section */ +.slazero2 .ebb-formulae-section .section-header { + background: linear-gradient(90deg, rgba(0, 100, 150, 0.6) 0%, rgba(20, 20, 20, 0.8) 100%); + border-color: #00aacc; + color: #00cccc !important; +} +.slazero2 .ebb-formulae-section .section-header i { + color: #00cccc; +} +.slazero2 .flux-indicator { + font-size: 12px; + color: #aaa; + font-family: "SLA Borg UI", sans-serif; + font-weight: normal; + letter-spacing: 0; + text-transform: none; +} +.slazero2 .flux-indicator strong { + color: #00cccc; +} +.slazero2 .ebb-discipline-badge { + background: rgba(0, 100, 150, 0.5) !important; + border-color: #00aacc !important; + color: #00cccc !important; +} +.slazero2 .ebb-effect-row .ebb-effect-text { + color: #bbb; + font-size: 12px; + font-style: italic; +} +/* All action buttons inside ebb formula items share a base dark style */ +.slazero2 .ebb-formula-item .weapon-actions button { + background: rgba(20, 20, 30, 0.6); + border: 1px solid #444; + color: #aaa; + border-radius: 4px; + padding: 4px 10px; + cursor: pointer; + transition: all 0.2s; + display: flex; + align-items: center; + justify-content: center; + font-size: 13px; + line-height: 1; +} +.slazero2 .ebb-formula-roll-btn { + background: rgba(0, 100, 150, 0.4); + border: 1px solid #00aacc !important; + color: #00cccc !important; +} +.slazero2 .ebb-formula-roll-btn:hover { + background: rgba(0, 150, 200, 0.5) !important; + color: #fff !important; +} +.slazero2 .ebb-formula-edit-btn { + border-color: #555 !important; + color: #999 !important; +} +.slazero2 .ebb-formula-edit-btn:hover { + border-color: #aaa !important; + color: #fff !important; + background: rgba(255,255,255,0.1) !important; +} +.slazero2 .ebb-formula-delete-btn { + border-color: #662222 !important; + color: #cc4444 !important; + background: rgba(100, 0, 0, 0.2) !important; +} +.slazero2 .ebb-formula-delete-btn:hover { + border-color: #ff4444 !important; + color: #fff !important; + background: rgba(180, 0, 0, 0.35) !important; +} + +/* Flux control buttons */ +.slazero2 .ebb-flux-controls { + display: flex; + gap: 8px; + padding: 8px 12px; + border-top: 1px solid rgba(0, 170, 204, 0.2); + margin-top: 4px; +} +.slazero2 .ebb-flux-recover-btn, +.slazero2 .ebb-flux-full-btn { + background: rgba(0, 80, 100, 0.4); + border: 1px solid #00aacc; + color: #00cccc; + border-radius: 4px; + padding: 5px 12px; + font-size: 12px; + cursor: pointer; + font-family: "SLA Borg UI", sans-serif; + transition: background 0.2s; +} +.slazero2 .ebb-flux-recover-btn:hover, +.slazero2 .ebb-flux-full-btn:hover { + background: rgba(0, 130, 160, 0.5); + color: #fff; +} + +/* Ebb item sheet */ +.slazero2 .ebb-sheet-content .item-prop-full { + padding: 6px 12px; +} +.slazero2 .ebb-sheet-content .item-prop-full label { + display: block; + color: #888; + font-size: 11px; + text-transform: uppercase; + letter-spacing: 1px; + margin-bottom: 4px; +} +.slazero2 .ebb-sheet-content textarea { + width: 100%; + background: rgba(0, 0, 0, 0.3); + border: 1px solid #444; + color: #ccc; + border-radius: 4px; + padding: 6px 8px; + font-family: "SLA Borg UI", "Rajdhani", sans-serif; + font-size: 13px; + resize: vertical; +} +.slazero2 .ebb-sheet-content textarea:focus { + border-color: #00aacc; + outline: none; +} + +/* ═══════════════════════════════════════════════════════════ + EBB TAB LAYOUT + ═══════════════════════════════════════════════════════════ */ + +/* Tab header: flux tracker + skill side by side */ +.slazero2 .ebb-tab-header { + display: flex; + gap: 12px; + padding: 10px 10px 0; + margin-bottom: 12px; + flex-wrap: wrap; +} + +/* Flux status panel */ +.slazero2 .ebb-flux-status { + display: flex; + align-items: center; + gap: 10px; + background: rgba(0, 80, 100, 0.35); + border: 1px solid #00cccc; + border-radius: 6px; + padding: 8px 14px; + flex: 1; + min-width: 220px; +} +.slazero2 .ebb-flux-label { + color: #00cccc; + font-family: "SLA Borg Title", "Orbitron", sans-serif; + font-size: 16px; + letter-spacing: 2px; + text-shadow: 0 0 8px rgba(0, 204, 204, 0.6); + white-space: nowrap; +} +.slazero2 .ebb-flux-track { + display: flex; + align-items: center; + gap: 4px; +} +.slazero2 .ebb-flux-input { + width: 48px; + background: rgba(0, 0, 0, 0.5); + border: 1px solid #00cccc; + border-radius: 4px; + color: #00cccc !important; + font-size: 22px; + font-weight: bold; + text-align: center; + padding: 2px 4px; + -moz-appearance: textfield; +} +.slazero2 .ebb-flux-input::-webkit-inner-spin-button, +.slazero2 .ebb-flux-input::-webkit-outer-spin-button { -webkit-appearance: none; } +.slazero2 .ebb-flux-sep { + color: #666; + font-size: 18px; +} +.slazero2 .ebb-flux-max { + font-size: 22px; + color: #555; + font-weight: bold; +} +.slazero2 .ebb-flux-actions { + display: flex; + gap: 6px; + margin-left: auto; +} + +/* Ebb skill panel */ +.slazero2 .ebb-skill-panel { + background: rgba(20, 30, 50, 0.4); + border: 1px solid #336699; + border-radius: 6px; + padding: 8px 14px; + flex: 1; + min-width: 200px; + display: flex; + flex-direction: column; + gap: 6px; +} +.slazero2 .ebb-skill-label { + color: #7799cc; + font-family: "SLA Borg Title", "Orbitron", sans-serif; + font-size: 13px; + letter-spacing: 1.5px; + text-transform: uppercase; +} +.slazero2 .ebb-skill-controls { + display: flex; + align-items: center; + gap: 6px; +} +.slazero2 .ebb-skill-value { + font-size: 20px !important; +} +.slazero2 .ebb-tab-skill-roll { + padding: 4px 10px; + height: auto !important; + width: auto !important; + font-size: 13px !important; + gap: 4px; + display: flex; + align-items: center; +} +.slazero2 .ebb-dice-summary { + color: #556688; + font-size: 11px; + font-style: italic; +} + +/* Formulae section within the tab */ +.slazero2 .tab[data-tab="ebb"] .ebb-formulae-section { + padding: 0 10px 10px; +} +.slazero2 .tab[data-tab="ebb"] .ebb-formulae-section .section-header { + background: linear-gradient(90deg, rgba(0, 100, 150, 0.6) 0%, rgba(20, 20, 20, 0.8) 100%); + color: #00cccc; + display: flex; + align-items: center; + gap: 8px; +} +.slazero2 .ebb-formula-count { + margin-left: auto; + font-size: 11px; + color: #556; + font-style: italic; +} + +/* Discipline badge colours on the Ebb tab */ +.slazero2 .ebb-disc-awareness { background: rgba(100, 180, 255, 0.25); color: #64b4ff; border: 1px solid #64b4ff; } +.slazero2 .ebb-disc-blast { background: rgba(255, 102, 0, 0.25); color: #ff6600; border: 1px solid #ff6600; } +.slazero2 .ebb-disc-communicate { background: rgba(180, 100, 255, 0.25); color: #b464ff; border: 1px solid #b464ff; } +.slazero2 .ebb-disc-enhance { background: rgba( 80, 220, 120, 0.25); color: #50dc78; border: 1px solid #50dc78; } +.slazero2 .ebb-disc-heal { background: rgba( 80, 220, 180, 0.25); color: #50dcb4; border: 1px solid #50dcb4; } +.slazero2 .ebb-disc-protect { background: rgba(220, 200, 80, 0.25); color: #dcc850; border: 1px solid #dcc850; } +.slazero2 .ebb-disc-realityFold { background: rgba(220, 80, 220, 0.25); color: #dc50dc; border: 1px solid #dc50dc; } +.slazero2 .ebb-disc-senses { background: rgba(255, 220, 80, 0.25); color: #ffdc50; border: 1px solid #ffdc50; } +.slazero2 .ebb-disc-telekinesis { background: rgba( 80, 180, 255, 0.25); color: #50b4ff; border: 1px solid #50b4ff; } +.slazero2 .ebb-disc-thermal { background: rgba(255, 60, 60, 0.25); color: #ff3c3c; border: 1px solid #ff3c3c; } + +/* Formula icon — white SVGs on a dark tinted tile */ +.slazero2 .ebb-formula-icon { + width: 40px; + height: 40px; + object-fit: contain; + flex-shrink: 0; + background: rgba(0, 0, 0, 0.45); + border-radius: 4px; + padding: 3px; + border: 1px solid rgba(255,255,255,0.08); +} +.slazero2 .ebb-flux-cost { + color: #00cccc !important; +} +.slazero2 .ebb-flux-cost i { + color: #00cccc; + font-size: 10px; +} + +/* ═══════════════════════════════════════════════════════════ + INLINE DIE FACES (panic, critical, armor roll results) + ═══════════════════════════════════════════════════════════ */ + +.inline-die-face { + display: inline-flex; + align-items: center; + justify-content: center; + width: 22px; + height: 22px; + background: rgba(30, 30, 40, 0.85); + border: 1px solid #555; + border-radius: 4px; + font-family: "SLA Borg Tech", "Share Tech Mono", monospace; + font-size: 13px; + font-weight: bold; + color: #ccc; + margin: 0 2px; + vertical-align: middle; + text-shadow: none; + box-shadow: inset 0 1px 2px rgba(0,0,0,0.4); +} + +.inline-die-face.inline-die-success { + border-color: #44aa44; + color: #88ee88; + background: rgba(20, 60, 20, 0.7); + box-shadow: 0 0 4px rgba(68, 170, 68, 0.3), inset 0 1px 2px rgba(0,0,0,0.3); +} + +/* ═══════════════════════════════════════════════════════════ + EBB OUTCOME BLOCK IN CHAT CARDS + ═══════════════════════════════════════════════════════════ */ + +.yze-ebb-outcome { + margin-top: 6px; + border-radius: 4px; + padding: 6px 8px; + border: 1px solid rgba(0, 204, 204, 0.35); + background: rgba(0, 60, 80, 0.4); + font-family: var(--font-primary, "Signika", sans-serif); + font-size: 12px; + line-height: 1.45; +} + +.yze-ebb-outcome.ebb-outcome-failed { + border-color: rgba(180, 60, 60, 0.5); + background: rgba(60, 10, 10, 0.4); +} + +.ebb-outcome-header { + display: flex; + align-items: center; + gap: 6px; + margin-bottom: 4px; + flex-wrap: wrap; +} + +.ebb-outcome-disc { + background: rgba(0, 100, 140, 0.5); + border: 1px solid #00aacc; + color: #00cccc; + border-radius: 3px; + padding: 1px 6px; + font-size: 10px; + font-weight: bold; + letter-spacing: 0.5px; + text-transform: uppercase; + white-space: nowrap; + flex-shrink: 0; +} + +.ebb-outcome-status { + font-size: 11px; + font-weight: bold; + line-height: 1.3; +} +.ebb-outcome-status.success { + color: #44dd88; +} +.ebb-outcome-status.failed { + color: #dd4444; +} + +.ebb-outcome-effect { + color: #cce8f0; + font-size: 12px; + line-height: 1.45; + margin-top: 2px; + word-wrap: break-word; +} + +.ebb-outcome-effect.ebb-failed-note { + color: #aa7777; + font-style: italic; +} + +.ebb-outcome-meta { + margin-top: 4px; + display: flex; + flex-wrap: wrap; + gap: 8px; + font-size: 11px; + color: #88aacc; +} +.ebb-outcome-meta strong { + color: #aaccee; +} + +.ebb-outcome-catastrophe { + margin-top: 5px; + padding: 4px 7px; + border-radius: 3px; + background: rgba(160, 40, 40, 0.4); + border: 1px solid #cc4444; + color: #ffaaaa; + font-size: 11px; + font-weight: bold; + word-wrap: break-word; +} + + +/* ═══════════════════════════════════════════════════════════ + SLA INDUSTRIES — CHARACTER GENERATORS + ═══════════════════════════════════════════════════════════ */ + +.sla-gen-container { + display: flex; + flex-direction: column; + height: 100%; + background: #0f0f14; + color: #d0d0d0; + font-family: var(--font-primary, "Signika", sans-serif); + font-size: 13px; + overflow: hidden; +} + +.sla-npc-container { height: auto; } + +.gen-header { + background: rgba(255,102,0,0.12); + border-bottom: 2px solid #ff6600; + padding: 8px 14px; + display: flex; + align-items: center; + justify-content: space-between; + flex-shrink: 0; +} +.gen-title { + font-size: 15px; + font-weight: bold; + color: #ff6600; + letter-spacing: 1px; + text-transform: uppercase; +} +.gen-progress { display: flex; gap: 6px; align-items: center; } +.gen-progress-dot { + width: 10px; height: 10px; + border-radius: 50%; + background: #333; + border: 1px solid #555; + transition: all 0.2s; +} +.gen-progress-dot.active { background: rgba(255,102,0,0.5); border-color: #ff6600; } +.gen-progress-dot.current { background: #ff6600; box-shadow: 0 0 6px #ff6600; } + +.gen-step { + flex-direction: column; + flex: 1; + overflow-y: auto; + padding: 10px 14px; + gap: 8px; +} +.step-title { + font-size: 13px; + color: #ff6600; + font-weight: bold; + text-transform: uppercase; + letter-spacing: 0.5px; + margin-bottom: 4px; + border-bottom: 1px solid rgba(255,102,0,0.3); + padding-bottom: 4px; +} +.gen-hint { font-size: 11px; color: #888; font-style: italic; } +.points-banner { + text-align: center; + font-size: 13px; + color: #ccc; + background: rgba(0,0,0,0.3); + border: 1px solid #333; + border-radius: 4px; + padding: 5px; +} +.points-banner strong { color: #00d4ff; font-size: 16px; } + +/* Race Grid */ +.race-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); + gap: 7px; + flex: 1; + overflow-y: auto; +} +.race-card { + background: rgba(255,255,255,0.04); + border: 1px solid #333; + border-radius: 5px; + padding: 8px; + cursor: pointer; + transition: all 0.15s; + display: flex; + flex-direction: column; + gap: 3px; +} +.race-card:hover { border-color: #ff6600; background: rgba(255,102,0,0.08); } +.race-card.selected { border-color: #ff6600; background: rgba(255,102,0,0.18); box-shadow: 0 0 8px rgba(255,102,0,0.3); } +.rc-icon { font-size: 20px; text-align: center; } +.rc-name { font-weight: bold; color: #fff; font-size: 12px; text-align: center; } +.rc-stats { font-size: 10px; color: #00d4ff; text-align: center; font-family: monospace; } +.rc-flavor { font-size: 10px; color: #999; line-height: 1.35; } +.rc-ebb { font-size: 10px; color: #cc88ff; font-weight: bold; margin-top: 2px; } + +/* Training Grid */ +.training-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); + gap: 7px; + flex: 1; + overflow-y: auto; +} +.training-card { + background: rgba(255,255,255,0.04); + border: 1px solid #333; + border-radius: 5px; + padding: 8px; + cursor: pointer; + transition: all 0.15s; + display: flex; + flex-direction: column; + gap: 3px; +} +.training-card:hover:not(.disabled) { border-color: #00aacc; background: rgba(0,170,200,0.08); } +.training-card.selected { border-color: #00aacc; background: rgba(0,170,200,0.18); box-shadow: 0 0 8px rgba(0,170,200,0.3); } +.training-card.disabled { opacity: 0.35; cursor: not-allowed; } +.tc-icon { font-size: 18px; text-align: center; } +.tc-name { font-weight: bold; color: #fff; font-size: 12px; text-align: center; } +.tc-flavor { font-size: 10px; color: #999; line-height: 1.35; } +.tc-skills { font-size: 10px; color: #88ddaa; font-family: monospace; } +.tc-ebb-only { font-size: 10px; color: #cc88ff; } + +/* Attribute table */ +.attr-table { display: flex; flex-direction: column; gap: 5px; } +.attr-row { + display: flex; + align-items: center; + gap: 8px; + padding: 5px 8px; + background: rgba(255,255,255,0.03); + border-radius: 4px; + border: 1px solid #2a2a2a; +} +.attr-label { width: 130px; color: #ccc; } +.attr-base { width: 55px; font-size: 11px; color: #666; } +.attr-val { width: 24px; text-align: center; font-size: 16px; font-weight: bold; color: #00d4ff; } +.attr-cap { font-size: 11px; color: #555; } +.attr-btn { + width: 24px; height: 24px; + background: rgba(255,102,0,0.15); + border: 1px solid rgba(255,102,0,0.4); + border-radius: 3px; + color: #ff9955; + cursor: pointer; + font-size: 14px; + line-height: 1; + display: flex; align-items: center; justify-content: center; +} +.attr-btn:hover { background: rgba(255,102,0,0.3); } +.derived-preview { text-align: center; font-size: 12px; color: #999; padding: 6px 0; } +.derived-preview strong { color: #00d4ff; } + +/* Skill table */ +.skill-gen-table { display: flex; flex-direction: column; gap: 4px; flex: 1; overflow-y: auto; } +.skill-group { display: flex; flex-direction: column; gap: 3px; } +.skill-group-header { + font-size: 11px; + text-transform: uppercase; + letter-spacing: 1px; + color: #888; + border-bottom: 1px solid #2a2a2a; + padding-bottom: 2px; + margin-top: 4px; +} +.skill-gen-row { + display: flex; + align-items: center; + gap: 6px; + padding: 3px 6px; + background: rgba(255,255,255,0.02); + border-radius: 3px; +} +.sg-label { width: 100px; color: #ccc; font-size: 12px; } +.sg-training { width: 70px; font-size: 10px; color: #88ddaa; } +.sg-val { width: 20px; text-align: center; font-weight: bold; color: #00d4ff; font-size: 14px; } +.sg-cap { font-size: 10px; color: #555; } +.sg-btn { + width: 20px; height: 20px; + background: rgba(0,170,200,0.1); + border: 1px solid rgba(0,170,200,0.3); + border-radius: 3px; + color: #00aacc; + cursor: pointer; + font-size: 13px; + line-height: 1; + display: flex; align-items: center; justify-content: center; +} +.sg-btn:hover { background: rgba(0,170,200,0.25); } + +/* Details form */ +.details-form { display: flex; flex-direction: column; gap: 10px; flex: 1; overflow-y: auto; } +.df-row { display: flex; flex-direction: column; gap: 4px; } +.df-row label { font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; color: #888; } +.df-row input, .df-row textarea { + background: rgba(255,255,255,0.06); + border: 1px solid #333; + border-radius: 4px; + color: #d0d0d0; + padding: 5px 8px; + font-size: 12px; + font-family: inherit; +} +.df-row input:focus, .df-row textarea:focus { border-color: #ff6600; outline: none; } +.df-row textarea { resize: vertical; } +.df-name-row { display: flex; gap: 8px; } +.df-name-row input { flex: 1; } + +/* Summary */ +.summary-grid { display: flex; flex-direction: column; gap: 5px; flex: 1; overflow-y: auto; } +.summary-section { + display: flex; + gap: 10px; + padding: 5px 8px; + background: rgba(255,255,255,0.03); + border-radius: 4px; + border-left: 2px solid #ff6600; +} +.sum-label { width: 100px; font-size: 11px; color: #888; text-transform: uppercase; letter-spacing: 0.5px; flex-shrink: 0; } +.sum-val { color: #d0d0d0; font-size: 12px; } + +/* Navigation */ +.gen-nav { + display: flex; + justify-content: flex-end; + gap: 8px; + padding-top: 8px; + border-top: 1px solid #222; + flex-shrink: 0; + margin-top: 4px; +} +.gen-btn-primary, .gen-btn-secondary, .gen-btn-create { + padding: 5px 14px; + border-radius: 4px; + cursor: pointer; + font-size: 12px; + font-weight: bold; + border: 1px solid; + transition: all 0.15s; +} +.gen-btn-primary { background: rgba(255,102,0,0.2); border-color: #ff6600; color: #ff9955; } +.gen-btn-primary:hover:not(:disabled) { background: rgba(255,102,0,0.4); } +.gen-btn-primary:disabled { opacity: 0.35; cursor: not-allowed; } +.gen-btn-secondary { background: rgba(255,255,255,0.05); border-color: #444; color: #999; } +.gen-btn-secondary:hover { border-color: #777; color: #ccc; } +.gen-btn-create { background: rgba(0,200,100,0.15); border-color: #00cc66; color: #00ee77; font-size: 13px; padding: 6px 18px; } +.gen-btn-create:hover { background: rgba(0,200,100,0.3); box-shadow: 0 0 10px rgba(0,200,100,0.3); } + +/* NPC Creator */ +.npc-nav { justify-content: space-between; } +.mode-tabs { display: flex; gap: 4px; } +.mode-tab { + padding: 3px 10px; + border-radius: 3px; + border: 1px solid #444; + cursor: pointer; + font-size: 11px; + color: #888; + transition: all 0.15s; +} +.mode-tab.active { border-color: #ff6600; color: #ff6600; background: rgba(255,102,0,0.15); } +.npc-form { display: flex; flex-direction: column; gap: 8px; padding: 10px 14px; flex: 1; overflow-y: auto; } +.nf-row { display: flex; flex-direction: column; gap: 3px; } +.nf-row label { font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; color: #888; } +.nf-row select, .nf-row input[type="text"], .nf-row textarea { + background: rgba(255,255,255,0.06); + border: 1px solid #333; + border-radius: 4px; + color: #d0d0d0; + padding: 5px 8px; + font-size: 12px; + font-family: inherit; +} +.nf-row select:focus, .nf-row input:focus, .nf-row textarea:focus { border-color: #00aacc; outline: none; } +.nf-row textarea { resize: vertical; } +.nf-row-full {} +.nf-section-header { + font-size: 11px; + text-transform: uppercase; + letter-spacing: 1px; + color: #00aacc; + border-bottom: 1px solid rgba(0,170,200,0.3); + padding-bottom: 4px; + margin-top: 4px; +} +.threat-slider { width: 100%; accent-color: #ff6600; } +.threat-label { color: #ff6600; font-weight: bold; font-size: 15px; margin-left: 6px; } +.threat-desc { font-size: 10px; color: #555; } + + + + + +/* ═══════════════════════════════════════════════════════════ + ZERO ENGINE — Mk2 DARK SHEET + High contrast · Grayscale · Readable + ═══════════════════════════════════════════════════════════ */ + +/* ── PALETTE (mostly greyscale, one red accent) ──────────── */ +.sla-mk2 { + --c-bg: #0a0a0a; + --c-panel: #101010; + --c-border: #303030; + --c-border2: #1c1c1c; + --c-text: #dedad3; /* primary text — warm near-white */ + --c-text2: #a09c96; /* secondary text */ + --c-text3: #5e5a57; /* dim labels */ + --c-red: #cc1818; /* single accent: SLA red */ + --c-red-dim: #4a1010; + --c-white: #f2ede8; /* numbers, names */ + background: var(--c-bg); + color: var(--c-text); + font-family: "SLA Borg UI", "Rajdhani", "Orbitron", sans-serif; + font-size: 14px; + display: flex; flex-direction: column; height: 100%; overflow: hidden; +} + +/* ── HEADER ──────────────────────────────────────────────── */ +.sla-mk2 .mk2-header { + display: flex; align-items: stretch; + background: #0a0a0a; border-bottom: 2px solid var(--c-red); flex-shrink: 0; +} +.sla-mk2 .mk2-portrait { + width: 76px; height: 76px; object-fit: cover; flex-shrink: 0; + border-right: 2px solid var(--c-red); + filter: saturate(0.2) contrast(1.3) brightness(0.9); +} +.sla-mk2 .mk2-identity { flex: 1; padding: 7px 12px; display: flex; flex-direction: column; gap: 6px; } +.sla-mk2 .mk2-name { + background: transparent; border: none; border-bottom: 1px solid #3a2a2a; + color: var(--c-white); font-family: "SLA Borg Title", "Oswald", sans-serif; + font-size: 22px; text-transform: uppercase; letter-spacing: 3px; width: 100%; padding: 0; +} +.sla-mk2 .mk2-name:focus { outline: none; border-bottom-color: var(--c-red); } +.sla-mk2 .mk2-id-row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; } +.sla-mk2 .mk2-field-pair { + display: flex; align-items: center; gap: 4px; + background: rgba(255,255,255,0.04); border: 1px solid var(--c-border); + border-radius: 3px; padding: 2px 8px; font-size: 11px; color: var(--c-text3); + text-transform: uppercase; letter-spacing: 0.5px; +} +.sla-mk2 .mk2-field-pair input { + background: transparent; border: none; color: var(--c-white); font-family: "SLA Borg Tech", monospace; + font-size: 13px; font-weight: bold; width: 50px; text-align: right; +} +.sla-mk2 .mk2-race-tag { + background: rgba(255,255,255,0.05); border: 1px solid var(--c-border); + border-radius: 3px; padding: 2px 8px; font-size: 11px; color: var(--c-text); + text-transform: uppercase; letter-spacing: 1px; +} +.sla-mk2 .mk2-race-tag.dim { color: var(--c-text3); } + +/* ── INITIATIVE COLUMN ───────────────────────────────────── */ +.sla-mk2 .mk2-init-col { + display: flex; flex-direction: column; align-items: stretch; + border-left: 2px solid var(--c-red); width: 90px; flex-shrink: 0; +} +.sla-mk2 .mk2-init-label { + font-family: "SLA Borg Title", "Oswald", sans-serif; + font-size: 9px; letter-spacing: 3px; text-transform: uppercase; + color: var(--c-text3); text-align: center; padding: 4px 2px; + border-bottom: 1px solid var(--c-border2); background: #0a0a0a; + writing-mode: horizontal-tb; +} +.sla-mk2 .mk2-init-btn { + flex: 1; font-size: 11px; font-weight: bold; text-transform: uppercase; + letter-spacing: 1px; cursor: pointer; border: none; + font-family: "SLA Borg UI", sans-serif; transition: filter 0.1s; + border-bottom: 1px solid rgba(0,0,0,0.3); +} +.sla-mk2 .mk2-init-btn:last-child { border-bottom: none; } +.sla-mk2 .mk2-init-btn.red { background: rgba(180,20,20,0.35); color: #ff9999; } +.sla-mk2 .mk2-init-btn.grey { background: rgba(60,60,60,0.4); color: #b0b0b0; } +.sla-mk2 .mk2-init-btn:hover { filter: brightness(1.35); } + +/* ── VITALS ──────────────────────────────────────────────── */ +.sla-mk2 .mk2-vitals { + display: flex; align-items: center; gap: 5px; padding: 6px 8px; + background: #0a0a0a; border-bottom: 1px solid var(--c-border2); flex-shrink: 0; +} +.sla-mk2 .mk2-vital { + display: flex; align-items: baseline; gap: 3px; + background: rgba(255,255,255,0.03); border: 1px solid var(--c-border2); + border-radius: 3px; padding: 4px 9px; +} +.sla-mk2 .vl { + font-size: 10px; text-transform: uppercase; letter-spacing: 1.5px; + color: var(--c-text3); margin-right: 4px; font-weight: bold; + font-family: "SLA Borg UI", "Rajdhani", sans-serif; +} +.sla-mk2 .vnum { + font-family: "SLA Borg Tech", monospace; font-size: 24px; font-weight: bold; + background: transparent; border: none; text-align: center; width: 36px; + color: var(--c-white); +} +.sla-mk2 .vnum.red { color: #e05050; } +.sla-mk2 .vnum.blue { color: #7090e0; } +.sla-mk2 .vnum.orange { color: #e08030; } +.sla-mk2 .vnum.purple { color: #c080f0; } +.sla-mk2 .vnum.dim { color: var(--c-text3); font-size: 16px; } +.sla-mk2 .vsep { color: var(--c-border); font-size: 18px; } +.sla-mk2 .mk2-vital.red { border-color: rgba(180,40,40,0.4); } +.sla-mk2 .mk2-vital.blue { border-color: rgba(60,80,180,0.4); } +.sla-mk2 .mk2-vital.orange { border-color: rgba(160,80,20,0.4); } +.sla-mk2 .mk2-vital.purple { border-color: rgba(140,60,200,0.35); } +.sla-mk2 .mk2-crit-col { margin-left: auto; display: flex; flex-direction: column; gap: 4px; } +.sla-mk2 .mk2-crit-btn { + padding: 4px 11px; font-size: 10px; text-transform: uppercase; letter-spacing: 0.5px; + cursor: pointer; border-radius: 3px; font-family: "SLA Borg UI", sans-serif; + font-weight: bold; border: 1px solid; +} +.sla-mk2 .mk2-crit-btn.red { background: rgba(180,20,20,0.18); border-color: #6a1010; color: #dd5555; } +.sla-mk2 .mk2-crit-btn.purple { background: rgba(120,50,200,0.12); border-color: #552299; color: #c080f0; } + +/* ── TABS ────────────────────────────────────────────────── */ +.sla-mk2 .mk2-nav { + display: flex; background: #090909; border-bottom: 2px solid var(--c-border2); flex-shrink: 0; +} +.sla-mk2 .mk2-tab { + padding: 8px 16px; font-size: 12px; font-weight: bold; text-transform: uppercase; + letter-spacing: 2px; color: var(--c-text3); cursor: pointer; + border-right: 1px solid var(--c-border2); + font-family: "SLA Borg Title","Oswald",sans-serif; position: relative; transition: color 0.1s; +} +.sla-mk2 .mk2-tab:hover { color: var(--c-text2); } +.sla-mk2 .mk2-tab.active { color: var(--c-white); background: rgba(255,255,255,0.04); } +.sla-mk2 .mk2-tab.active::after { + content:''; position:absolute; bottom:-2px; left:0; right:0; height:2px; background: var(--c-red); +} +.sla-mk2 .mk2-tab.ebb-tab.active { color: #c080f0; } +.sla-mk2 .mk2-tab.ebb-tab.active::after { background: #9944cc; } + +/* ── PANELS ──────────────────────────────────────────────── */ +.sla-mk2 .mk2-panel { display: none; padding: 9px; overflow-y: auto; flex: 1; } +.sla-mk2 .mk2-panel.active { display: block; } +.sla-mk2 .mk2-subhead { + font-family: "SLA Borg Title", "Oswald", sans-serif; + font-size: 13px; text-transform: uppercase; letter-spacing: 2.5px; color: var(--c-text2); + border-bottom: 1px solid var(--c-border); padding-bottom: 5px; margin-bottom: 8px; + display: flex; justify-content: space-between; align-items: center; +} +.sla-mk2 .mk2-none-msg { font-size: 11px; color: var(--c-text3); font-style: italic; padding: 3px 6px; } +.sla-mk2 .mk2-add { font-size: 11px; color: var(--c-red); cursor: pointer; font-family: sans-serif; letter-spacing: 0; font-weight: normal; } + +/* ── ATTRIBUTE + SKILL GRID ──────────────────────────────── */ +.sla-mk2 .mk2-attr-skill-grid { + display: grid; grid-template-columns: repeat(4, 1fr); gap: 5px; margin-bottom: 12px; +} +.sla-mk2 .mk2-attr-col { + background: var(--c-panel); border: 1px solid var(--c-border2); + border-top: 2px solid var(--c-border); border-radius: 3px; overflow: hidden; +} +/* Subtle top border tints per attribute — greyscale with a hint of color */ +.sla-mk2 .str-color { border-top-color: #704040; } +.sla-mk2 .agi-color { border-top-color: #606040; } +.sla-mk2 .wit-color { border-top-color: #404060; } +.sla-mk2 .emp-color { border-top-color: #405540; } + +.sla-mk2 .mk2-attr-head { + display: flex; align-items: center; justify-content: space-between; + padding: 5px 7px; border-bottom: 1px solid var(--c-border2); background: rgba(255,255,255,0.04); +} +.sla-mk2 .mk2-attr-abbr { + font-family: "SLA Borg Title", "Oswald", sans-serif; + font-size: 14px; font-weight: bold; letter-spacing: 2px; color: var(--c-text2); +} +.sla-mk2 .mk2-attr-val { + font-family: "SLA Borg Tech", monospace; font-size: 28px; font-weight: bold; + color: var(--c-white); background: transparent; border: none; text-align: center; width: 34px; +} +.sla-mk2 .mk2-attr-roll { + background: transparent; border: none; color: var(--c-text3); cursor: pointer; + font-size: 11px; padding: 2px 4px; transition: color 0.1s; +} +.sla-mk2 .mk2-attr-roll:hover { color: var(--c-red); } + +/* Skill entries — LARGER, readable text */ +.sla-mk2 .mk2-skill-entry { + display: flex; align-items: center; gap: 5px; padding: 4px 7px; + border-bottom: 1px solid var(--c-border2); +} +.sla-mk2 .mk2-skill-entry:last-child { border-bottom: none; } +.sla-mk2 .mk2-skill-entry.ebb-sk { background: rgba(140,60,200,0.07); } +.sla-mk2 .mk2-sk-roll { + background: transparent; border: none; color: var(--c-text3); cursor: pointer; + font-size: 10px; padding: 0; width: 14px; flex-shrink: 0; transition: color 0.1s; +} +.sla-mk2 .mk2-sk-roll:hover { color: var(--c-red); } +.sla-mk2 .mk2-sk-name { + flex: 1; font-size: 13px; font-weight: bold; color: var(--c-text); + white-space: nowrap; overflow: hidden; font-family: "SLA Borg UI","Rajdhani",sans-serif; + letter-spacing: 0.5px; +} +.sla-mk2 .mk2-sk-dots { display: flex; gap: 3px; flex-shrink: 0; } +.sla-mk2 .mk2-dot { + width: 9px; height: 9px; border-radius: 50%; + border: 1px solid #444; /* visible empty dots */ background: transparent; +} +.sla-mk2 .mk2-dot.on { background: #c0bab4; border-color: #c0bab4; } /* greyscale filled */ +.sla-mk2 .str-color .mk2-dot.on { background: #c08080; border-color: #c08080; } +.sla-mk2 .agi-color .mk2-dot.on { background: #b0a870; border-color: #b0a870; } +.sla-mk2 .wit-color .mk2-dot.on { background: #7090c0; border-color: #7090c0; } +.sla-mk2 .emp-color .mk2-dot.on { background: #70b090; border-color: #70b090; } +.sla-mk2 .ebb-dot.on { background: #c080f0 !important; border-color: #c080f0 !important; } +.sla-mk2 .mk2-sk-num { + font-size: 14px; font-weight: bold; color: var(--c-white); /* HIGH CONTRAST */ + width: 16px; text-align: right; flex-shrink: 0; +} + +/* ── SPECIALTIES ─────────────────────────────────────────── */ +.sla-mk2 .mk2-spec { + display: flex; align-items: center; gap: 8px; padding: 4px 6px; + border-left: 2px solid var(--c-border); margin-bottom: 3px; + background: rgba(255,255,255,0.02); +} +.sla-mk2 .mk2-spec.racial { border-left-color: var(--c-red); } +.sla-mk2 .mk2-spec-name { + font-size: 13px; font-weight: bold; color: var(--c-white); /* HIGH CONTRAST */ + width: 160px; flex-shrink: 0; +} +.sla-mk2 .mk2-spec-fx { flex: 1; font-size: 12px; color: var(--c-text2); } +.sla-mk2 .mk2-ico { font-size: 11px; cursor: pointer; color: var(--c-text3); flex-shrink: 0; } +.sla-mk2 .mk2-ico:hover { color: var(--c-text2); } +.sla-mk2 .mk2-ico.del:hover { color: var(--c-red); } + +/* ── ARMOUR ON STATS PANEL ───────────────────────────────── */ +.sla-mk2 .mk2-armor-stat-row { + display: flex; align-items: center; gap: 8px; padding: 5px 8px; + background: rgba(255,255,255,0.03); border: 1px solid var(--c-border2); + border-radius: 3px; margin-bottom: 4px; +} +.sla-mk2 .mk2-armor-stat-name { flex: 1; font-size: 13px; font-weight: bold; color: var(--c-white); } +.sla-mk2 .mk2-armor-stat-val { font-size: 12px; color: var(--c-text2); font-family: "SLA Borg Tech",monospace; } + +/* ── WEAPONS ─────────────────────────────────────────────── */ +.sla-mk2 .mk2-wpn { + background: rgba(255,255,255,0.025); border: 1px solid var(--c-border2); + border-left: 3px solid #6a1818; border-radius: 0 3px 3px 0; padding: 7px 10px; margin-bottom: 4px; +} +.sla-mk2 .mk2-wpn.mk2-wpn-empty { + border-left-color: #553010; opacity: 0.75; +} +.sla-mk2 .mk2-wpn-row1 { + display: flex; align-items: center; gap: 6px; flex-wrap: nowrap; +} +.sla-mk2 .mk2-wpn-row2 { + display: flex; align-items: center; gap: 6px; flex-wrap: nowrap; margin-top: 5px; +} +.sla-mk2 .mk2-wpn-name { + font-family: "SLA Borg Title","Oswald",sans-serif; + font-size: 16px; font-weight: bold; color: var(--c-white); + text-transform: uppercase; flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; +} +.sla-mk2 .mk2-stat-pill { + background: rgba(255,255,255,0.06); border: 1px solid var(--c-border); border-radius: 2px; + padding: 2px 8px; font-size: 12px; font-family: "SLA Borg Tech",monospace; + color: var(--c-text2); white-space: nowrap; flex-shrink: 0; +} +.sla-mk2 .mk2-stat-pill.orange { border-color: #5a3010; color: #c08040; } +.sla-mk2 .mk2-stat-pill.teal { border-color: #105040; color: #50b0a0; } +.sla-mk2 .mk2-ammo-empty-pill { + font-size: 10px; font-weight: bold; color: #ff5500; background: rgba(150,40,0,0.2); + border: 1px solid #882200; border-radius: 2px; padding: 1px 6px; white-space: nowrap; flex-shrink: 0; +} +.sla-mk2 .mk2-caliber { + background: rgba(180,20,20,0.08); border: 1px solid #3a1010; border-radius: 2px; + padding: 2px 8px; font-size: 11px; font-family: "SLA Borg Tech",monospace; + color: #996666; text-transform: uppercase; flex-shrink: 0; +} +/* Round-type button group */ +.sla-mk2 .mk2-rb-group { + display: flex; gap: 0; flex-shrink: 0; +} +.sla-mk2 .mk2-rb-group .mk2-rb:not(:last-child) { border-right: none; border-radius: 2px 0 0 2px; } +.sla-mk2 .mk2-rb-group .mk2-rb:last-child { border-radius: 0 2px 2px 0; } +.sla-mk2 .mk2-rb-group .mk2-rb:not(:first-child):not(:last-child) { border-radius: 0; } + +/* Round buttons — compact toggles */ +.sla-mk2 .mk2-rb { + padding: 3px 10px; font-size: 11px; font-weight: bold; text-transform: uppercase; + border-radius: 2px; cursor: pointer; font-family: "SLA Borg Tech",monospace; + border: 1px solid var(--c-border); background: rgba(255,255,255,0.04); color: var(--c-text3); + transition: filter 0.1s; white-space: nowrap; flex-shrink: 0; +} +.sla-mk2 .mk2-rb.rb-std { border-color: #2a5a2a; color: #70c070; background: rgba(40,100,40,0.18); } +.sla-mk2 .mk2-rb.rb-ap { border-color: #2a3a6a; color: #6090d0; background: rgba(40,60,140,0.18); } +.sla-mk2 .mk2-rb.rb-he { border-color: #6a2a1a; color: #d06040; background: rgba(140,50,20,0.18); } +.sla-mk2 .mk2-rb:hover { filter: brightness(1.4); } + +/* Attack button — the main call-to-action */ +.sla-mk2 .mk2-attack-btn { + margin-left: auto; flex-shrink: 0; +} + +/* Locked/empty state */ +.sla-mk2 .mk2-btn-locked, +.sla-mk2 .mk2-btn-locked:disabled { + background: rgba(50,50,50,0.2) !important; border-color: #444 !important; + color: #555 !important; cursor: not-allowed; +} + +/* Action buttons */ +.sla-mk2 .mk2-action-btn { + padding: 4px 14px; font-size: 12px; font-weight: bold; text-transform: uppercase; + letter-spacing: 0.5px; cursor: pointer; border-radius: 2px; + font-family: "SLA Borg UI","SLA Borg Title",sans-serif; border: 1px solid; transition: filter 0.1s; flex-shrink: 0; +} +.sla-mk2 .mk2-action-btn.red { background: rgba(200,20,20,0.22); border-color: #6a1010; color: #ee6060; } +.sla-mk2 .mk2-action-btn.teal { background: rgba(20,100,90,0.15); border-color: #104040; color: #60b0a8; } +.sla-mk2 .mk2-action-btn.grey { background: rgba(80,80,80,0.15); border-color: #444; color: #aaa; } +.sla-mk2 .mk2-action-btn.purple { background: rgba(120,50,200,0.15); border-color: #551199; color: #cc88ff; min-width: 110px; text-align: center; } +.sla-mk2 .mk2-action-btn:hover { filter: brightness(1.35); } + +.sla-mk2 .mk2-armor { display: flex; align-items: center; gap: 8px; padding: 5px 8px; background: rgba(255,255,255,0.03); border: 1px solid var(--c-border2); border-radius: 3px; margin-bottom: 4px; } +.sla-mk2 .mk2-armor-name { flex: 1; font-size: 13px; font-weight: bold; color: var(--c-white); } + +/* ── FINANCES ────────────────────────────────────────────── */ +.sla-mk2 .mk2-credits-big { text-align: center; font-family: "SLA Borg Tech",monospace; font-size: 48px; font-weight: bold; color: var(--c-white); text-shadow: 0 0 16px rgba(255,200,100,0.12); padding: 12px 0 10px; border-bottom: 1px solid var(--c-border2); margin-bottom: 12px; letter-spacing: 2px; } +.sla-mk2 .mk2-c-unit { font-size: 20px; color: var(--c-text3); } +.sla-mk2 .mk2-debt { font-size: 13px; color: #dd4444; margin-left: 14px; vertical-align: middle; } +.sla-mk2 .mk2-fin-cols { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; } +.sla-mk2 .mk2-fin-col { background: rgba(255,255,255,0.03); border: 1px solid var(--c-border2); border-radius: 3px; padding: 8px; } +.sla-mk2 .mk2-fin-head { font-family: "SLA Borg Title","Oswald",sans-serif; font-size: 11px; text-transform: uppercase; letter-spacing: 1.5px; padding-bottom: 5px; margin-bottom: 6px; font-weight: bold; } +.sla-mk2 .mk2-fin-head.green { color: #60b080; border-bottom: 1px solid rgba(80,160,100,0.25); } +.sla-mk2 .mk2-fin-head.red { color: #c05050; border-bottom: 1px solid rgba(160,50,50,0.25); } +.sla-mk2 .mk2-fin-row { display: flex; justify-content: space-between; align-items: center; padding: 5px 0; border-bottom: 1px solid rgba(255,255,255,0.06); font-size: 14px; } +.sla-mk2 .mk2-fin-row label { color: var(--c-text); font-family: "SLA Borg UI","Oswald",sans-serif; font-size: 13px; } +.sla-mk2 .mk2-fin-amount { display: flex; align-items: center; gap: 4px; flex-shrink: 0; } +.sla-mk2 .mk2-fin-amount input { background: rgba(255,255,255,0.06); border: 1px solid var(--c-border); border-radius: 2px; color: var(--c-white); font-family:"SLA Borg Tech",monospace; font-size: 14px; text-align: right; width: 80px; padding: 3px 6px; } +.sla-mk2 .mk2-fin-amount > span { color: var(--c-text3); font-size: 12px; font-family:"SLA Borg Tech",monospace; } +.sla-mk2 .mk2-fin-total { display: flex; justify-content: space-between; align-items: center; padding: 6px 0 3px; font-size: 15px; font-weight: bold; border-top: 1px solid var(--c-border); margin-top: 4px; font-family: "SLA Borg Title","Oswald",sans-serif; text-transform: uppercase; letter-spacing: 1px; } +.sla-mk2 .green { color: #60c080; } .sla-mk2 .red { color: #dd5555; } .sla-mk2 .orange { color: #c08040; } .sla-mk2 .fw { font-weight: bold; } +.sla-mk2 .mk2-net-row { display: flex; justify-content: space-between; align-items: center; background: rgba(255,255,255,0.04); border: 1px solid var(--c-border); border-radius: 3px; padding: 10px 16px; margin-top: 10px; } +.sla-mk2 .mk2-net-label { font-family:"SLA Borg Title","Oswald",sans-serif; font-size: 12px; text-transform: uppercase; letter-spacing: 2px; color: var(--c-text2); } +.sla-mk2 .mk2-net-val { font-family:"SLA Borg Tech",monospace; font-size: 32px; font-weight: bold; letter-spacing: 2px; } +.sla-mk2 .mk2-net-val.green { color: #60c080; } .sla-mk2 .mk2-net-val.red { color: #dd5555; } +.sla-mk2 .mk2-ammo-list { display: flex; flex-direction: column; gap: 3px; } +.sla-mk2 .mk2-ammo-row { display: flex; align-items: center; gap: 8px; padding: 4px 7px; background: rgba(255,255,255,0.03); border-radius: 2px; border: 1px solid var(--c-border2); } +.sla-mk2 .mk2-ammo-cal { color: var(--c-text2); text-transform: uppercase; font-size: 11px; font-weight: bold; width: 55px; } +.sla-mk2 .mk2-ammo-type { font-size: 11px; font-weight: bold; text-transform: uppercase; } +.sla-mk2 .mk2-ammo-type.standard { color: #70c070; } .sla-mk2 .mk2-ammo-type.ap { color: #7090c0; } .sla-mk2 .mk2-ammo-type.he { color: #c07050; } +.sla-mk2 .mk2-ammo-qty { font-family:"SLA Borg Tech",monospace; font-size: 18px; font-weight: bold; color: var(--c-white); } +.sla-mk2 .mk2-ammo-cost { color: var(--c-text3); font-size: 11px; } + +/* ── EBB ─────────────────────────────────────────────────── */ +.sla-mk2 .mk2-ebb-row { display: flex; align-items: center; gap: 10px; padding: 8px 10px; background: rgba(100,40,180,0.1); border: 1px solid rgba(120,50,200,0.25); border-left: 3px solid #6622bb; border-radius: 0 3px 3px 0; margin-bottom: 5px; } +.sla-mk2 .mk2-ebb-disc { font-size: 10px; text-transform: uppercase; letter-spacing: 1px; border-radius: 2px; padding: 4px 10px; background: rgba(120,50,200,0.2); border: 1px solid rgba(140,60,220,0.45); color: #bb88ff; white-space: nowrap; font-family: "SLA Borg Title","Oswald",sans-serif; min-width: 64px; text-align: center; } +.sla-mk2 .mk2-ebb-info { flex: 1; min-width: 0; } +.sla-mk2 .mk2-ebb-name { display: block; font-size: 17px; font-weight: bold; color: var(--c-white); text-transform: uppercase; font-family: "SLA Borg Title","Oswald",sans-serif; } +.sla-mk2 .mk2-ebb-meta { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 3px; } +.sla-mk2 .mk2-ebb-flux { font-size: 13px; color: #cc88ff; font-family: "SLA Borg Tech",monospace; } +.sla-mk2 .mk2-ebb-suc { font-size: 13px; color: var(--c-text2); } +.sla-mk2 .mk2-ebb-range, .sla-mk2 .mk2-ebb-dur { font-size: 12px; color: var(--c-text3); } +.sla-mk2 .mk2-ebb-effect { font-size: 12px; color: var(--c-text2); margin-top: 3px; font-style: italic; } +.sla-mk2 .mk2-ebb-actions { display: flex; align-items: center; gap: 8px; flex-shrink: 0; } +.sla-mk2 .mk2-channel-btn { font-size: 13px; padding: 5px 14px; } + +/* ── GEAR / NOTES ────────────────────────────────────────── */ +.sla-mk2 .mk2-gear-row { display: flex; align-items: center; gap: 6px; padding: 5px 8px; border-bottom: 1px solid var(--c-border2); font-size: 14px; } +.sla-mk2 .mk2-gear-name { flex: 1; color: var(--c-text); font-family: "SLA Borg UI","Rajdhani",sans-serif; font-size: 14px; } +.sla-mk2 .mk2-bio { width: 100%; height: 230px; background: rgba(255,255,255,0.03); border: 1px solid var(--c-border); color: var(--c-text); /* READABLE */ font-family: "SLA Borg Book",serif; font-size: 13px; padding: 9px; resize: vertical; line-height: 1.6; border-radius: 3px; } + +/* ── Mk1 finances + armor name ───────────────────────────── */ +.slazero2 .armor-name-display { font-size: 12px; margin-bottom: 3px; } +.slazero2 .equipped-armor-name { color: #00d4ff; font-weight: bold; text-transform: uppercase; letter-spacing: 0.5px; } +.slazero2 .equipped-armor-name.none { color: #555; font-style: italic; text-transform: none; } +.slazero2 .finances-section { padding: 12px; } +.slazero2 .finances-credits-hero { display: flex; align-items: center; gap: 16px; background: rgba(0,0,0,0.3); border: 2px solid rgba(255,102,0,0.3); border-radius: 6px; padding: 12px 18px; margin-bottom: 14px; } +.slazero2 .fin-credit-label { font-size: 11px; color: #888; text-transform: uppercase; letter-spacing: 1px; } +.slazero2 .fin-credit-value { font-size: 28px; font-weight: bold; color: #00d4ff; } +.slazero2 .fin-credit-value input { background: transparent; border: none; color: #00d4ff; font-size: 26px; font-weight: bold; width: 100px; } +.slazero2 .fin-debt-badge { background: rgba(220,40,40,0.2); border: 1px solid #cc2222; border-radius: 4px; padding: 3px 8px; color: #ff4444; font-size: 12px; font-weight: bold; margin-left: auto; } +/* ── Showing Off hero panel ─────────────────────────────── */ +.slazero2 .finances-showing-off-hero { + display: flex; align-items: center; gap: 16px; + background: rgba(255,200,0,0.07); border: 2px solid rgba(255,190,0,0.35); + border-radius: 6px; padding: 10px 18px; margin-bottom: 14px; +} +.slazero2 .fin-so-label { font-size: 11px; color: #cc9900; text-transform: uppercase; letter-spacing: 1px; } +.slazero2 .fin-so-value-wrap { display: flex; align-items: baseline; gap: 4px; } +.slazero2 .fin-so-value { background: transparent; border: none; color: #ffdd44; font-size: 26px; font-weight: bold; width: 60px; } +.slazero2 .fin-so-unit { font-size: 13px; color: #aa8800; } +.slazero2 .fin-so-milestone { + margin-left: auto; background: rgba(220,40,0,0.2); border: 1px solid #cc2200; + border-radius: 4px; padding: 3px 8px; color: #ff4422; font-size: 11px; font-weight: bold; +} +.slazero2 .finances-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-bottom: 12px; } +.slazero2 .fin-panel { background: rgba(0,0,0,0.25); border: 1px solid rgba(255,102,0,0.2); border-radius: 6px; padding: 10px; } +.slazero2 .fin-panel-title { font-size: 12px; font-weight: bold; text-transform: uppercase; letter-spacing: 1px; padding-bottom: 6px; margin-bottom: 8px; border-bottom: 1px solid rgba(255,255,255,0.1); } +.slazero2 .income-panel .fin-panel-title { color: #44cc88; } +.slazero2 .expense-panel .fin-panel-title { color: #ff6644; } +.slazero2 .fin-row { display: flex; justify-content: space-between; align-items: center; padding: 4px 0; border-bottom: 1px solid rgba(255,255,255,0.05); font-size: 12px; } +.slazero2 .fin-row label { color: #aaa; } +.slazero2 .fin-row input { background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.1); border-radius: 3px; color: #ddd; text-align: right; width: 80px; padding: 2px 5px; font-size: 12px; } +.slazero2 .fin-row.fin-total { border-top: 2px solid rgba(255,102,0,0.3); border-bottom: none; margin-top: 4px; padding-top: 6px; font-weight: bold; } +.slazero2 .fin-total-val { font-size: 15px; color: #ff9955; } +.slazero2 .income-total .fin-total-val { color: #44cc88; } +.slazero2 .expense-total .fin-total-val { color: #ff6644; } +.slazero2 .fin-btax { color: #ff9900; font-weight: bold; } +.slazero2 .fin-net-bar { display: flex; justify-content: space-between; align-items: center; background: rgba(255,102,0,0.08); border: 2px solid rgba(255,102,0,0.25); border-radius: 6px; padding: 10px 18px; } +.slazero2 .fin-net-label { font-size: 12px; text-transform: uppercase; letter-spacing: 1px; color: #888; } +.slazero2 .fin-net-val { font-size: 22px; font-weight: bold; } +.slazero2 .net-positive { color: #44cc88; } .slazero2 .net-negative { color: #ff4444; } +/* bullet tax chat */ +.bullet-tax-line { color: #886600; font-style: italic; } +.btax-total { color: #cc4400; font-weight: bold; } + +/* ── Gear section: amber/yellow accent ── */ +.slazero2 .weapons-section:has(h2 .fa-toolbox) h2, +.slazero2 .gear-section-header { + background: linear-gradient(90deg, #aa7700 0%, transparent 100%) !important; +} + +/* ── Drugs section: magenta/purple accent ── */ +.slazero2 .weapons-section:has(h2 .fa-pills) h2, +.slazero2 .drugs-section-header { + background: linear-gradient(90deg, #880077 0%, transparent 100%) !important; +} + +/* Drug item — richer status display */ +.slazero2 .drug-item { + position: relative; +} + +.slazero2 .drug-item.drug-active { + border-left-color: #00dd66; + background: rgba(0,90,40,0.22); + box-shadow: inset 0 0 12px rgba(0,200,80,0.08); +} + +.slazero2 .drug-item.drug-withdrawal { + border-left-color: #dd5500; + background: rgba(80,30,0,0.22); + box-shadow: inset 0 0 12px rgba(220,80,0,0.08); +} + +/* ── LAD Account locked expense row ─────────────────────── */ +.slazero2 .lad-expense-row label { + color: #888; font-style: italic; +} +.slazero2 .fin-amount-locked input { + background: #1a1a1a; color: #666; cursor: not-allowed; + border-color: #333; opacity: 0.7; +} +.slazero2 .fin-amount-locked span { + color: #666; +} + +/* ── PC Status button (GM only) ─────────────────────────── */ +.slazero2 .pc-status-btn { + display: block; width: 100%; margin-top: 4px; + background: rgba(80,120,180,0.2); border: 1px solid rgba(80,120,200,0.4); + color: #88aadd; border-radius: 3px; padding: 3px 6px; + font-size: 10px; cursor: pointer; text-transform: uppercase; letter-spacing: 0.5px; +} +.slazero2 .pc-status-btn:hover { + background: rgba(80,120,180,0.4); color: #aaccff; +} + +/* ── Active Conditions Row ──────────────────────────────── */ +.slazero2 .active-conditions-row { + display: flex; flex-wrap: wrap; gap: 5px; margin-top: 6px; padding: 5px 0; +} +.slazero2 .condition-tag { + display: flex; align-items: center; gap: 6px; + background: rgba(200,80,0,0.15); border: 1px solid rgba(200,100,0,0.4); + border-radius: 3px; padding: 4px 8px; font-size: 11px; color: #e0a060; + font-family: "SLA Borg UI", sans-serif; +} +.slazero2 .condition-tag-body { + display: flex; flex-direction: column; gap: 1px; +} +.slazero2 .condition-label { + text-transform: uppercase; letter-spacing: 0.5px; font-weight: bold; +} +.slazero2 .condition-penalty { + font-size: 10px; opacity: 0.85; font-style: italic; text-transform: none; letter-spacing: 0; +} +.slazero2 .condition-tag.sla-bleeding { border-color: #aa2222; background: rgba(160,20,20,0.2); color: #ff6666; } +.slazero2 .condition-tag.sla-stunned { border-color: #aaaa22; background: rgba(140,140,20,0.18); color: #ffff88; } +.slazero2 .condition-tag.sla-winded { border-color: #888822; background: rgba(120,120,20,0.15); color: #dddd66; } +.slazero2 .condition-tag.sla-broken-arm,.slazero2 .condition-tag.sla-broken-leg { border-color: #882200; background: rgba(120,40,0,0.2); color: #ff8844; } +.slazero2 .condition-tag.sla-panicking { border-color: #882288; background: rgba(120,20,120,0.2); color: #ff88ff; } +.slazero2 .condition-tag.sla-shaken { border-color: #664488; background: rgba(100,40,120,0.18); color: #cc88ff; } +.slazero2 .condition-clear-btn { + background: transparent; border: none; color: inherit; cursor: pointer; + font-size: 10px; padding: 0 0 0 3px; opacity: 0.7; line-height: 1; +} +.slazero2 .condition-clear-btn:hover { opacity: 1; } + +/* ── Stress max display ─────────────────────────────────── */ +.slazero2 .stress-max-display { + color: #555; font-size: 14px; margin-left: 2px; align-self: center; +} + +/* ── Drug withdrawal warning ────────────────────────────── */ +.slazero2 .drug-withdrawal-warning { + background: rgba(180,80,0,0.15); border: 1px solid rgba(220,100,0,0.35); + border-radius: 4px; padding: 6px 10px; margin-bottom: 8px; + color: #ff8833; font-size: 12px; display: flex; align-items: center; gap: 8px; +} +.slazero2 .drug-withdrawal-warning i { font-size: 14px; } + +/* ══════════════════════════════════════════════════════════════ + BPN PANEL — original sheet + ══════════════════════════════════════════════════════════════ */ +.slazero2 .bpn-panel { + margin-top: 16px; + border: 2px solid #334; + border-radius: 5px; + overflow: hidden; + background: rgba(0,0,0,0.25); +} + +/* Color theming by BPN type */ +.slazero2 .bpn-panel.bpn-type-blue { border-color: #1155aa; } +.slazero2 .bpn-panel.bpn-type-red { border-color: #aa1111; } +.slazero2 .bpn-panel.bpn-type-white { border-color: #888; } +.slazero2 .bpn-panel.bpn-type-gold { border-color: #aa8800; } +.slazero2 .bpn-panel.bpn-type-silver { border-color: #778899; } +.slazero2 .bpn-panel.bpn-type-black { border-color: #222; background: rgba(0,0,0,0.5); } +.slazero2 .bpn-panel.bpn-type-grey { border-color: #555; } +.slazero2 .bpn-panel.bpn-type-violet { border-color: #6622aa; } + +.slazero2 .bpn-header { + display: flex; justify-content: space-between; align-items: center; + flex-wrap: wrap; gap: 8px; + padding: 8px 12px; + background: rgba(255,255,255,0.04); + border-bottom: 1px solid rgba(255,255,255,0.08); +} +.slazero2 .bpn-header-left { display: flex; align-items: center; gap: 10px; } +.slazero2 .bpn-header-right { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; } + +.slazero2 .bpn-type-badge { + font-family: "SLA Borg Title","Oswald",sans-serif; + font-size: 13px; font-weight: bold; text-transform: uppercase; letter-spacing: 2px; + padding: 3px 10px; border-radius: 2px; white-space: nowrap; +} +.slazero2 .bpn-type-blue .bpn-type-badge { background: rgba(20,80,180,0.3); color: #88bbff; border: 1px solid #3366cc; } +.slazero2 .bpn-type-red .bpn-type-badge { background: rgba(180,20,20,0.3); color: #ff8888; border: 1px solid #cc3333; } +.slazero2 .bpn-type-white .bpn-type-badge { background: rgba(200,200,200,0.15); color: #ddd; border: 1px solid #888; } +.slazero2 .bpn-type-gold .bpn-type-badge { background: rgba(180,140,0,0.25); color: #ffdd66; border: 1px solid #aa9900; } +.slazero2 .bpn-type-silver .bpn-type-badge { background: rgba(120,140,160,0.2); color: #aabbcc; border: 1px solid #778899; } +.slazero2 .bpn-type-black .bpn-type-badge { background: rgba(0,0,0,0.6); color: #999; border: 1px solid #333; } +.slazero2 .bpn-type-grey .bpn-type-badge { background: rgba(80,80,80,0.3); color: #aaa; border: 1px solid #555; } +.slazero2 .bpn-type-violet .bpn-type-badge { background: rgba(100,30,170,0.3); color: #cc88ff; border: 1px solid #7733bb; } + +.slazero2 .bpn-code-input { + background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.15); + border-radius: 3px; color: #ddd; font-family: "SLA Borg Tech",monospace; + font-size: 14px; letter-spacing: 1px; padding: 4px 8px; width: 140px; +} +.slazero2 .bpn-type-select, .slazero2 .bpn-status-select { + background: rgba(0,0,0,0.4); border: 1px solid rgba(255,255,255,0.15); + border-radius: 3px; color: #ccc; font-size: 12px; padding: 4px 6px; +} +.slazero2 .bpn-reward-field { display: flex; align-items: center; gap: 5px; font-size: 12px; color: #aaa; } +.slazero2 .bpn-reward-input { width: 70px; } + +.slazero2 .bpn-body { padding: 8px 12px; display: flex; flex-direction: column; gap: 8px; } +.slazero2 .bpn-field { display: flex; flex-direction: column; gap: 3px; } +.slazero2 .bpn-field-label { font-size: 11px; text-transform: uppercase; letter-spacing: 1px; color: #888; } +.slazero2 .bpn-textarea { + background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.1); + border-radius: 3px; color: #ccc; font-size: 12px; padding: 6px 8px; + resize: vertical; font-family: inherit; line-height: 1.5; +} +.slazero2 .bpn-textarea:focus { border-color: rgba(255,255,255,0.25); outline: none; } + +/* ══ BPN PANEL — Mk2 sheet ══ */ +.sla-mk2 .mk2-bpn { + border: 2px solid #334; border-radius: 3px; margin-bottom: 10px; + background: rgba(0,0,0,0.2); overflow: hidden; +} +.sla-mk2 .mk2-bpn.bpn-type-blue { border-color: #1155aa; } +.sla-mk2 .mk2-bpn.bpn-type-red { border-color: #aa1111; } +.sla-mk2 .mk2-bpn.bpn-type-white { border-color: #888; } +.sla-mk2 .mk2-bpn.bpn-type-gold { border-color: #aa8800; } +.sla-mk2 .mk2-bpn.bpn-type-silver { border-color: #778899; } +.sla-mk2 .mk2-bpn.bpn-type-black { border-color: #222; background: rgba(0,0,0,0.5); } +.sla-mk2 .mk2-bpn.bpn-type-grey { border-color: #555; } +.sla-mk2 .mk2-bpn.bpn-type-violet { border-color: #6622aa; } + +.sla-mk2 .mk2-bpn-head { + display: flex; align-items: center; gap: 8px; flex-wrap: wrap; + padding: 7px 10px; background: rgba(255,255,255,0.04); + border-bottom: 1px solid rgba(255,255,255,0.07); +} +.sla-mk2 .mk2-bpn-type { + font-family: "SLA Borg Title","Oswald",sans-serif; + font-size: 12px; font-weight: bold; text-transform: uppercase; letter-spacing: 2px; + color: #aaa; padding: 2px 8px; border-radius: 2px; + background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.1); + white-space: nowrap; flex-shrink: 0; +} +.sla-mk2 .mk2-bpn-code { + background: rgba(255,255,255,0.06); border: 1px solid var(--c-border); + border-radius: 2px; color: var(--c-white); font-family: "SLA Borg Tech",monospace; + font-size: 13px; padding: 3px 7px; width: 130px; +} +.sla-mk2 .mk2-bpn-select { + background: rgba(0,0,0,0.4); border: 1px solid var(--c-border); + border-radius: 2px; color: var(--c-text2); font-size: 12px; padding: 3px 5px; +} +.sla-mk2 .mk2-bpn-text { + display: block; width: 100%; background: rgba(255,255,255,0.03); + border: none; border-top: 1px solid rgba(255,255,255,0.06); + color: var(--c-text); font-size: 12px; padding: 6px 10px; + resize: vertical; font-family: inherit; line-height: 1.5; +} +.sla-mk2 .mk2-bpn-text:focus { background: rgba(255,255,255,0.05); outline: none; } +.sla-mk2 .mk2-bpn-text::placeholder { color: var(--c-text3); } + +/* ══════════════════════════════════════════════════════════════════ + SLA INDUSTRIES — GAME PAUSED OVERLAY + ══════════════════════════════════════════════════════════════════ */ + +/* Hide Foundry's default pause element while ours is active */ +body.ze-pause-active #pause { + opacity: 0 !important; + pointer-events: none !important; +} + +/* Full-screen overlay */ +body > .ze-pause-root { + position: fixed; + inset: 0; + z-index: 9999; + display: flex; + align-items: center; + justify-content: center; + pointer-events: none; + background: radial-gradient(circle at center, rgba(120,0,0,0.07), transparent 30%), + rgba(0,0,0,0.18); +} + +body > .ze-pause-root .ze-pause-stage { + width: min(34vw, 420px); + height: min(44vw, 520px); + display: flex; + align-items: center; + justify-content: center; +} + +body > .ze-pause-root .ze-pause-svg { + width: 100%; + height: 100%; + overflow: visible; + filter: drop-shadow(0 0 22px rgba(180, 20, 20, 0.3)); +} + +/* Animations */ +body > .ze-pause-root .ze-pause-standby { + animation: zePausePulse 1.8s ease-in-out infinite; +} +body > .ze-pause-root .ze-pause-standby2 { + animation: zePausePulse 1.8s ease-in-out infinite 0.9s; +} +body > .ze-pause-root .ze-pause-scan { + transform-origin: 0 0; + animation: zePauseScan 3s linear infinite; +} +body > .ze-pause-root .ze-pause-logo-core { + animation: zePauseLogoPulse 2.6s ease-in-out infinite; +} +body > .ze-pause-root .ze-pause-core-glow { + animation: zePauseCoreGlow 3s ease-in-out infinite; +} +body > .ze-pause-root .ze-sat, +body > .ze-pause-root .ze-sat-inner { + animation: zePauseSatPulse 1.4s ease-in-out infinite; +} + +@keyframes zePausePulse { + 0%, 100% { opacity: 0.35; } + 50% { opacity: 1; } +} +@keyframes zePauseScan { + 0% { transform: rotate(0deg); opacity: 0.08; } + 50% { opacity: 0.32; } + 100% { transform: rotate(360deg); opacity: 0.08; } +} +@keyframes zePauseLogoPulse { + 0%, 100% { opacity: 0.82; filter: brightness(0.9); } + 50% { opacity: 1; filter: brightness(1.25); } +} +@keyframes zePauseCoreGlow { + 0%, 100% { opacity: 0.55; } + 50% { opacity: 0.9; } +} +@keyframes zePauseSatPulse { + 0%, 100% { opacity: 0.7; r: 4; } + 50% { opacity: 1; r: 5.5; } +} + +/* ══ Vevaphon Morph Form Panel ══ */ +.slazero2 .morph-form-panel { + background: rgba(0,0,0,0.2); border: 1px solid #333; border-radius: 5px; + padding: 10px 12px; margin-bottom: 14px; +} +.slazero2 .morph-form-buttons { + display: flex; gap: 8px; margin: 8px 0; +} +.slazero2 .morph-form-btn { + flex: 1; padding: 8px 4px; background: rgba(50,50,80,0.3); + border: 2px solid #444; border-radius: 5px; color: #aaa; + font-size: 11px; font-weight: bold; text-transform: uppercase; + cursor: pointer; transition: all 0.2s; text-align: center; line-height: 1.4; +} +.slazero2 .morph-form-btn:hover { border-color: #888; color: #fff; } +.slazero2 .morph-form-btn.morph-active { + border-color: #00d4ff; color: #00d4ff; background: rgba(0,180,255,0.12); + box-shadow: 0 0 10px rgba(0,200,255,0.25); +} +.slazero2 .instability-warning { font-size: 11px; color: #ff8800; margin-left: 8px; } +.slazero2 .instability-warning.active { animation: pulse-lethal 1.5s infinite; } +.slazero2 .morph-instability-bar { display: flex; align-items: center; gap: 8px; margin-top: 8px; } +.slazero2 .instability-track { display: flex; gap: 3px; flex: 1; } +.slazero2 .instability-pip { + width: 16px; height: 16px; border-radius: 3px; + border: 1px solid #444; background: rgba(255,255,255,0.05); flex-shrink: 0; +} +.slazero2 .instability-pip.pip-filled { background: #ff8800; border-color: #cc6600; } +.slazero2 .instability-pip.pip-filled.pip-danger { background: #ff2200; border-color: #cc1100; animation: pulse-lethal 1.5s infinite; } +.slazero2 .instability-roll-btn { + background: rgba(255,100,0,0.2); border: 1px solid #cc5500; border-radius: 4px; + color: #ff8840; font-size: 11px; font-weight: bold; padding: 4px 10px; + cursor: pointer; white-space: nowrap; transition: all 0.2s; +} +.slazero2 .instability-roll-btn:hover { background: rgba(255,100,0,0.35); } +/* Vevaphon instability vital box */ +.slazero2 .vevaphon-vital { border-color: #ff8800; background: rgba(80,40,0,0.15); } +.slazero2 .vevaphon-vital label { color: #ff8800; } +.slazero2 .vevaphon-vital .instability-value { border-color: #cc6600; color: #ffaa44; font-size: 18px; font-weight: bold; width: 40px; text-align: center; background: rgba(80,40,0,0.3); border: 1px solid; border-radius: 3px; padding: 4px; } + +/* Morph SHIFT button on specialty entries */ +.slazero2 .morph-shift-btn { + padding: 2px 10px; font-size: 10px; font-weight: bold; text-transform: uppercase; + letter-spacing: 0.5px; border-radius: 3px; cursor: pointer; transition: all 0.2s; + background: rgba(0,180,255,0.1); border: 1px solid #0090cc; color: #44bbff; + white-space: nowrap; flex: 0 0 auto; max-width: 110px; +} +.slazero2 .morph-shift-btn:hover { background: rgba(0,180,255,0.25); } +.slazero2 .morph-shift-btn.morph-shift-active { + background: rgba(0,212,255,0.2); border-color: #00d4ff; color: #00d4ff; + box-shadow: 0 0 8px rgba(0,200,255,0.3); +} + +/* Natural weapon STRIKE button */ +.slazero2 .natural-weapon-roll-btn { + padding: 2px 10px; font-size: 10px; font-weight: bold; text-transform: uppercase; + letter-spacing: 0.5px; border-radius: 3px; cursor: pointer; transition: all 0.2s; + background: rgba(255,80,0,0.15); border: 1px solid #cc4400; color: #ff7733; + white-space: nowrap; flex: 0 0 auto; +} +.slazero2 .natural-weapon-roll-btn:hover { + background: rgba(255,80,0,0.3); border-color: #ff6600; color: #ffaa55; +} + +/* ══════════════════════════════════════════════════════════════════════════════ + PLAYER CHARACTER FLAG — Bio Tab + ══════════════════════════════════════════════════════════════════════════════ */ +.slazero2 .pc-flag-row { + display: flex; + align-items: center; + gap: 12px; + padding: 8px 12px; + margin-bottom: 14px; + background: rgba(255, 200, 0, 0.07); + border: 1px solid rgba(255, 200, 0, 0.25); + border-radius: 5px; +} +.slazero2 .pc-flag-label { + display: flex; + align-items: center; + gap: 7px; + font-size: 13px; + font-weight: bold; + color: #ffcc00; + cursor: pointer; + white-space: nowrap; +} +.slazero2 .pc-flag-label input[type="checkbox"] { + width: 16px; + height: 16px; + accent-color: #ffcc00; + cursor: pointer; +} +.slazero2 .pc-flag-hint { + font-size: 11px; + color: #888; + font-style: italic; +} + +/* ══════════════════════════════════════════════════════════════════════════════ + GM FINANCE LEDGER + ══════════════════════════════════════════════════════════════════════════════ */ +.gm-finance-tool .gm-finance-wrap { + padding: 12px; + display: flex; + flex-direction: column; + gap: 10px; +} +.gm-finance-tool .gm-finance-header-note { + font-size: 12px; + color: #aaa; + padding: 6px 10px; + background: rgba(255,255,255,0.04); + border-left: 3px solid #ffcc00; + border-radius: 3px; +} +.gm-finance-tool .gm-finance-header-note strong { color: #ffcc00; } + +.gm-finance-tool .gm-finance-table { + width: 100%; + border-collapse: collapse; + font-size: 13px; +} +.gm-finance-tool .gm-finance-table thead tr { + background: rgba(255, 200, 0, 0.1); + border-bottom: 1px solid rgba(255,200,0,0.3); +} +.gm-finance-tool .gm-finance-table thead th { + padding: 7px 10px; + text-align: left; + color: #ffcc00; + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.5px; +} +.gm-finance-tool .gm-finance-table tbody tr { + border-bottom: 1px solid rgba(255,255,255,0.06); + transition: background 0.15s; +} +.gm-finance-tool .gm-finance-table tbody tr:hover { + background: rgba(255,255,255,0.04); +} +.gm-finance-tool .gm-finance-table td { + padding: 8px 10px; + color: #ccc; +} +.gm-finance-tool .gf-name { + font-weight: bold; + color: #eee; +} +.gm-finance-tool .gf-balance { + color: #88ccff; + font-weight: bold; + font-size: 14px; +} +.gm-finance-tool .gf-income { color: #44cc66; } +.gm-finance-tool .gf-expenses { color: #cc5555; } +.gm-finance-tool .gf-net { font-weight: bold; font-size: 14px; } +.gm-finance-tool .net-positive { color: #44cc66; } +.gm-finance-tool .net-negative { color: #cc3333; } + +.gm-finance-tool .gm-finance-calc-btn { + padding: 4px 10px; + font-size: 11px; + font-weight: bold; + background: rgba(255,200,0,0.15); + border: 1px solid #ffcc00; + color: #ffcc00; + border-radius: 3px; + cursor: pointer; + transition: all 0.2s; + white-space: nowrap; +} +.gm-finance-tool .gm-finance-calc-btn:hover { + background: rgba(255,200,0,0.3); + box-shadow: 0 0 8px rgba(255,200,0,0.25); +} + +.gm-finance-tool .gm-finance-empty { + text-align: center; + padding: 20px; + color: #777; + font-style: italic; +} + +.gm-finance-tool .gm-finance-footer { + display: flex; + justify-content: flex-end; + padding-top: 4px; +} +.gm-finance-tool .gm-finance-calc-all-btn { + padding: 6px 16px; + font-size: 12px; + font-weight: bold; + background: rgba(255,200,0,0.2); + border: 1px solid #ffcc00; + color: #ffcc00; + border-radius: 4px; + cursor: pointer; + transition: all 0.2s; +} +.gm-finance-tool .gm-finance-calc-all-btn:hover { + background: rgba(255,200,0,0.35); + box-shadow: 0 0 10px rgba(255,200,0,0.3); +} + +/* ══════════════════════════════════════════════════════════════════════════════ + GROUP NPC GENERATOR + ══════════════════════════════════════════════════════════════════════════════ */ +.group-npc-tool .window-content { padding: 0; overflow: hidden; } +.group-npc-tool .gnpc-wrap { + display: flex; flex-direction: column; height: 100%; background: #1a1a1a; +} +/* Toolbar */ +.group-npc-tool .gnpc-toolbar { + padding: 8px 10px 6px; border-bottom: 1px solid #2a2a2a; + display: flex; flex-direction: column; gap: 6px; flex-shrink: 0; +} +.group-npc-tool .gnpc-type-row { display: flex; gap: 5px; flex-wrap: wrap; } +.group-npc-tool .gnpc-type-btn { + padding: 4px 12px; font-size: 12px; font-weight: bold; border-radius: 4px; + border: 1px solid; cursor: pointer; background: transparent; + transition: all 0.15s; white-space: nowrap; +} +.group-npc-tool .gnpc-type-btn:hover { opacity: 0.85; } +.group-npc-tool .gnpc-action-row { + display: flex; align-items: center; gap: 8px; flex-wrap: wrap; +} +.group-npc-tool .gnpc-count-lbl { + display: flex; align-items: center; gap: 5px; font-size: 12px; color: #aaa; +} +/* Threat level slider row */ +.group-npc-tool .gnpc-threat-lbl { + display: flex; align-items: center; gap: 6px; + font-size: 12px; color: #aaa; flex-shrink: 0; +} +.group-npc-tool .gnpc-threat-slider { + -webkit-appearance: none; appearance: none; + width: 110px; height: 4px; border-radius: 2px; + background: linear-gradient(to right, #00ccff 0%, #00ccff 20%, #333 20%, #333 100%); + outline: none; cursor: pointer; +} +.group-npc-tool .gnpc-threat-slider::-webkit-slider-thumb { + -webkit-appearance: none; appearance: none; + width: 12px; height: 12px; border-radius: 50%; + background: #00ccff; border: 2px solid #0088aa; cursor: pointer; + box-shadow: 0 0 4px rgba(0,200,255,0.5); +} +.group-npc-tool .gnpc-threat-slider::-moz-range-thumb { + width: 12px; height: 12px; border-radius: 50%; + background: #00ccff; border: 2px solid #0088aa; cursor: pointer; +} +.group-npc-tool .gnpc-threat-val { + font-size: 11px; color: #00ccff; font-family: monospace; + white-space: nowrap; min-width: 90px; +} +/* Threat level badge on NPC cards */ +.group-npc-tool .gnpc-tlvl-tag { + font-size: 11px; font-weight: bold; color: #00ccff; + background: rgba(0,200,255,0.12); border: 1px solid rgba(0,200,255,0.35); + border-radius: 10px; padding: 1px 6px; white-space: nowrap; flex-shrink: 0; + text-shadow: 0 0 6px rgba(0,200,255,0.6); +} +/* Initiative roll row */ +.group-npc-tool .gnpc-init-row { + display: flex; align-items: center; gap: 6px; margin: 3px 0 1px; +} +.group-npc-tool .gnpc-init-btn { + background: rgba(50,200,100,0.12) !important; border: 1px solid #33aa66 !important; + color: #44cc88 !important; font-weight: bold; font-size: 10px; padding: 2px 6px; + border-radius: 3px; cursor: pointer; flex-shrink: 0; +} +.group-npc-tool .gnpc-init-btn:hover { background: rgba(50,200,100,0.28) !important; } +.group-npc-tool .gnpc-init-pool { + font-size: 11px; color: #55bb77; font-family: monospace; flex-shrink: 0; +} +.group-npc-tool .gnpc-init-result { + display: flex; align-items: center; gap: 2px; flex-wrap: wrap; min-height: 18px; +} +.group-npc-tool .gnpc-die { + display: inline-flex; align-items: center; justify-content: center; + width: 15px; height: 15px; border-radius: 3px; + font-size: 9px; font-weight: bold; line-height: 1; + background: rgba(255,255,255,0.07); border: 1px solid #2a2a2a; color: #777; +} +.group-npc-tool .gnpc-die.gnpc-die-success { + background: rgba(50,200,100,0.22); border-color: #44cc88; color: #44ff88; + box-shadow: 0 0 5px rgba(68,204,136,0.45); +} +.group-npc-tool .gnpc-init-successes { + font-size: 11px; font-weight: bold; color: #44cc88; + font-family: monospace; margin-left: 4px; flex-shrink: 0; +} +.group-npc-tool .gnpc-count-sel { + background: rgba(255,255,255,0.08); border: 1px solid #444; + color: #eee; border-radius: 3px; padding: 2px 4px; font-size: 12px; +} +.group-npc-tool .gnpc-gen-btn { background:rgba(255,200,0,0.15); border:1px solid #ffcc00; color:#ffcc00; border-radius:3px; padding:3px 10px; font-size:12px; font-weight:bold; cursor:pointer; } +.group-npc-tool .gnpc-add-btn { background:rgba(0,180,100,0.12); border:1px solid #00bb66; color:#00bb66; border-radius:3px; padding:3px 10px; font-size:12px; cursor:pointer; } +.group-npc-tool .gnpc-clr-btn { background:rgba(200,50,50,0.1); border:1px solid #884444; color:#bb6666; border-radius:3px; padding:3px 10px; font-size:12px; cursor:pointer; margin-left:auto; } +.group-npc-tool .gnpc-gen-btn:hover { background:rgba(255,200,0,0.3); } +.group-npc-tool .gnpc-add-btn:hover { background:rgba(0,180,100,0.25); } +.group-npc-tool .gnpc-clr-btn:hover { background:rgba(200,50,50,0.25); } +/* Card list */ +.group-npc-tool .gnpc-list { + flex: 1; overflow-y: auto; padding: 8px 10px; + display: grid; grid-template-columns: repeat(auto-fill, minmax(360px, 1fr)); + gap: 8px; align-content: start; +} +.group-npc-tool .gnpc-empty { + text-align: center; padding: 40px 20px; color: #555; + font-style: italic; grid-column: 1 / -1; +} +/* NPC card */ +.group-npc-tool .gnpc-card { + background: rgba(255,255,255,0.03); border-radius: 5px; + padding: 8px 10px; display: flex; flex-direction: column; gap: 5px; + transition: opacity 0.2s; +} +.group-npc-tool .gnpc-card.gnpc-defeated { opacity: 0.4; } +.group-npc-tool .gnpc-card-top { + display: flex; align-items: center; gap: 6px; +} +.group-npc-tool .gnpc-name-input { + flex: 1; min-width: 0; background: rgba(255,255,255,0.06); + border: 1px solid #333; border-radius: 3px; color: #eee; + padding: 3px 6px; font-size: 13px; font-weight: bold; +} +.group-npc-tool .gnpc-name-input:focus { border-color: #555; outline: none; } +.group-npc-tool .gnpc-type-tag { + font-size: 10px; padding: 2px 7px; border-radius: 10px; border: 1px solid; + white-space: nowrap; flex-shrink: 0; +} +.group-npc-tool .gnpc-threat-tag { font-size: 11px; color: #666; white-space: nowrap; flex-shrink: 0; } +.group-npc-tool .gnpc-card-btns { display: flex; gap: 3px; flex-shrink: 0; } +.group-npc-tool .gnpc-btn { + background: transparent; border: 1px solid #333; border-radius: 3px; + color: #888; padding: 2px 5px; cursor: pointer; font-size: 11px; transition: all 0.15s; +} +.group-npc-tool .gnpc-btn:hover { background: rgba(255,255,255,0.08); color: #ccc; } +.group-npc-tool .gnpc-remove-btn:hover { border-color: #cc4444; color: #cc4444; } +/* Stats row */ +.group-npc-tool .gnpc-card-stats { + display: flex; align-items: center; gap: 7px; flex-wrap: wrap; font-size: 11px; +} +.group-npc-tool .gnpc-stat-lbl { + display: flex; align-items: center; gap: 3px; color: #777; +} +.group-npc-tool .gnpc-stat-lbl-plain { color: #777; font-size: 11px; } +.group-npc-tool .gnpc-stat-input { + width: 34px; text-align: center; background: rgba(255,255,255,0.07); + border: 1px solid #2a2a2a; border-radius: 3px; color: #ddd; + padding: 2px 2px; font-size: 12px; +} +.group-npc-tool .gnpc-sep { color: #333; } +.group-npc-tool .gnpc-hp-group { + display: flex; align-items: center; gap: 3px; +} +.group-npc-tool .gnpc-hp-val { width: 30px; color: #88eeaa; } +.group-npc-tool .gnpc-hp-max { width: 30px; color: #aaa; } +.group-npc-tool .gnpc-hp-sep { color: #444; font-size: 11px; } +.group-npc-tool .gnpc-hp-btn { + background: rgba(255,255,255,0.06); border: 1px solid #333; border-radius: 3px; + color: #888; padding: 0 4px; cursor: pointer; font-size: 13px; line-height: 1.4; + flex-shrink: 0; transition: all 0.1s; +} +.group-npc-tool .gnpc-hp-btn:hover { background: rgba(255,255,255,0.15); color: #eee; } +.group-npc-tool .gnpc-hp-bar-wrap { + width: 50px; height: 5px; background: #222; border-radius: 3px; + overflow: hidden; flex-shrink: 0; +} +.group-npc-tool .gnpc-hp-bar { height: 100%; border-radius: 3px; transition: width 0.2s, background 0.3s; } +/* Abilities */ +.group-npc-tool .gnpc-abilities { display: flex; flex-direction: column; gap: 1px; } +.group-npc-tool .gnpc-ability { font-size: 10.5px; color: #888; padding: 0 2px; line-height: 1.4; } +.group-npc-tool .gnpc-ability i { color: #555; margin-right: 3px; } +/* Notes */ +.group-npc-tool .gnpc-notes-input { + width: 100%; background: rgba(255,255,255,0.03); border: 1px solid #222; + border-radius: 3px; color: #888; font-size: 11px; padding: 3px 6px; + resize: vertical; box-sizing: border-box; font-family: inherit; +} +.group-npc-tool .gnpc-notes-input:focus { border-color: #444; outline: none; color: #bbb; } +/* Footer */ +.group-npc-tool .gnpc-footer { + display: flex; align-items: center; justify-content: space-between; + padding: 6px 10px; border-top: 1px solid #2a2a2a; flex-shrink: 0; +} +.group-npc-tool .gnpc-tally { font-size: 12px; color: #666; } +.group-npc-tool .gnpc-footer-btns { display: flex; gap: 8px; } +.group-npc-tool .gnpc-reset-hp-btn { + padding: 4px 12px; font-size: 11px; background: rgba(255,100,100,0.1); + border: 1px solid #884444; color: #cc8888; border-radius: 3px; cursor: pointer; +} +.group-npc-tool .gnpc-reset-hp-btn:hover { background: rgba(255,100,100,0.2); } +.group-npc-tool .gnpc-create-btn { + padding: 4px 12px; font-size: 11px; font-weight: bold; + background: rgba(255,200,0,0.15); border: 1px solid #ffcc00; + color: #ffcc00; border-radius: 3px; cursor: pointer; +} +.group-npc-tool .gnpc-create-btn:hover { background: rgba(255,200,0,0.3); } + +/* Group NPC Generator — weapons & armor additions */ +.group-npc-tool .gnpc-weapons-block { + display: flex; flex-direction: column; gap: 2px; +} +.group-npc-tool .gnpc-weapon-row { + display: flex; align-items: baseline; gap: 5px; + font-size: 10.5px; padding: 1px 2px; +} +.group-npc-tool .gnpc-weapon-row.natural { color: #77bb66; } +.group-npc-tool .gnpc-weapon-row.conventional { color: #7799cc; } +.group-npc-tool .gnpc-weapon-icon { flex-shrink: 0; width: 14px; } +.group-npc-tool .gnpc-weapon-row strong { color: inherit; font-size: 11px; } +.group-npc-tool .gnpc-weapon-detail { color: #666; font-size: 10px; } +.group-npc-tool .gnpc-armor-row { + font-size: 10.5px; color: #aaa88a; padding: 1px 2px; +} +.group-npc-tool .gnpc-armor-row i { margin-right: 4px; color: #887755; } + +/* Group NPC — count as number input */ +.group-npc-tool .gnpc-count-sel { + width: 44px; text-align: center; + background: rgba(255,255,255,0.08); border: 1px solid #555; + color: #eee; border-radius: 3px; padding: 3px 4px; font-size: 13px; + font-weight: bold; +} + +/* ── GM Status Window ────────────────────────────────────────────────────── */ +.gm-status-window .gsw-wrap { + display: flex; flex-direction: column; gap: 0; + background: #111; color: #ddd; font-family: var(--font-primary, sans-serif); + min-width: 300px; +} +.gm-status-window .gsw-header-bar { + display: flex; align-items: center; justify-content: space-between; + padding: 6px 10px; background: rgba(255,255,255,0.05); + border-bottom: 1px solid #333; +} +.gm-status-window .gsw-post-chat { + font-size: 11px; padding: 3px 10px; + background: rgba(0,150,255,0.2); border: 1px solid #0099ff; + color: #66bbff; border-radius: 3px; cursor: pointer; +} +.gm-status-window .gsw-post-chat:hover { background: rgba(0,150,255,0.4); } +.gm-status-window .gsw-actor-list { display: flex; flex-direction: column; } +.gm-status-window .gsw-actor-row { + display: flex; gap: 10px; align-items: flex-start; + padding: 8px 10px; border-bottom: 1px solid #222; +} +.gm-status-window .gsw-actor-row:last-child { border-bottom: none; } +.gm-status-window .gsw-portrait { + width: 42px; height: 42px; border-radius: 50%; + object-fit: cover; border: 2px solid #444; flex-shrink: 0; + cursor: pointer; transition: border-color 0.15s; +} +.gm-status-window .gsw-portrait:hover { border-color: #ff6600; } +.gm-status-window .gsw-actor-body { flex: 1; min-width: 0; } +.gm-status-window .gsw-actor-name { + font-size: 13px; font-weight: bold; color: #ff6600; + cursor: pointer; margin-bottom: 3px; + white-space: nowrap; overflow: hidden; text-overflow: ellipsis; +} +.gm-status-window .gsw-actor-name:hover { text-decoration: underline; } +.gm-status-window .gsw-stats-row { + display: flex; gap: 12px; flex-wrap: wrap; + font-size: 12px; margin-bottom: 5px; +} +.gm-status-window .gsw-stat { font-variant-numeric: tabular-nums; } +.gm-status-window .gsw-conds-row { + display: flex; flex-wrap: wrap; gap: 4px; font-size: 11px; +} +.gm-status-window .gsw-cond-tag { + display: inline-flex; align-items: center; gap: 4px; + background: rgba(200,50,50,0.25); border: 1px solid #aa3333; + border-radius: 3px; padding: 2px 6px; color: #ff9999; +} +.gm-status-window .gsw-no-cond { color: #444; font-style: italic; font-size: 11px; } +.gm-status-window .gsw-remove-cond { + background: none; border: none; color: #ff6666; cursor: pointer; + font-size: 10px; padding: 0 1px; line-height: 1; +} +.gm-status-window .gsw-remove-cond:hover { color: #ff0000; } + + +/* ══════════════════════════════════════════════════════════════════════════ + SLA GM TOOLS — v1.7.0 + ══════════════════════════════════════════════════════════════════════════ */ + + +/* ── 💳 CREDIT DISTRIBUTION TOOL ──────────────────────────────────────────────────── */ +/* ============================================================ + SLA Credit Distribution Tool — .credit-tool-window scoped + Dark sci-fi aesthetic: navy/midnight backgrounds, cyan + orange neon + ============================================================ */ + +.credit-tool-window .window-content { + background: #0d1117; + color: #c9d1d9; + padding: 0; + font-family: "SLA Borg UI", "Rajdhani", "Oxanium", monospace, sans-serif; +} + +/* ── Wrapper ─────────────────────────────────────────────── */ +.credit-tool-window .ctw-wrapper { + display: flex; + flex-direction: column; + gap: 0; +} + +/* ── Section containers ──────────────────────────────────── */ +.credit-tool-window .ctw-section { + padding: 10px 12px; + border-bottom: 1px solid #1e2d3d; +} + +.credit-tool-window .ctw-section:last-child { + border-bottom: none; +} + +.credit-tool-window .ctw-section-title { + font-size: 10px; + font-weight: 600; + letter-spacing: 0.12em; + text-transform: uppercase; + color: #00d4ff; + margin: 0 0 8px 0; + padding-bottom: 5px; + border-bottom: 1px solid rgba(0, 212, 255, 0.25); +} + +/* ── Table ───────────────────────────────────────────────── */ +.credit-tool-window .ctw-table { + width: 100%; + border-collapse: collapse; + font-size: 11px; +} + +.credit-tool-window .ctw-header-row { + background: #111927; +} + +.credit-tool-window .ctw-th { + padding: 5px 6px; + text-align: center; + font-size: 9px; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; + color: #7d8fa1; + border-bottom: 1px solid #1e2d3d; + white-space: nowrap; +} + +.credit-tool-window .ctw-th-name { + text-align: left; +} + +.credit-tool-window .ctw-row { + transition: background 0.15s ease; + border-bottom: 1px solid #161d27; +} + +.credit-tool-window .ctw-row:hover { + background: rgba(0, 212, 255, 0.04); +} + +.credit-tool-window .ctw-row:last-child { + border-bottom: none; +} + +.credit-tool-window .ctw-cell { + padding: 5px 6px; + text-align: center; + vertical-align: middle; + font-family: "Courier New", "Consolas", monospace; + font-size: 11px; +} + +/* ── Name cell ───────────────────────────────────────────── */ +.credit-tool-window .ctw-name { + display: flex; + align-items: center; + gap: 6px; + padding: 5px 6px; + text-align: left; + white-space: nowrap; +} + +.credit-tool-window .ctw-portrait { + width: 28px; + height: 28px; + border-radius: 3px; + object-fit: cover; + border: 1px solid #2a3a4a; + flex-shrink: 0; +} + +.credit-tool-window .ctw-actor-name { + font-size: 11px; + font-weight: 600; + color: #e6edf3; + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 120px; +} + +.credit-tool-window .ctw-scl { + font-size: 9px; + color: #7d8fa1; + font-family: "Courier New", monospace; + flex-shrink: 0; +} + +/* ── Credits input ───────────────────────────────────────── */ +.credit-tool-window .ctw-credits-input { + width: 70px; + background: #0d1117; + border: 1px solid #2a3a4a; + border-radius: 3px; + color: #00d4ff; + font-family: "Courier New", "Consolas", monospace; + font-size: 11px; + font-weight: 700; + text-align: center; + padding: 3px 4px; + transition: border-color 0.15s ease, box-shadow 0.15s ease; +} + +.credit-tool-window .ctw-credits-input:focus { + outline: none; + border-color: #00d4ff; + box-shadow: 0 0 0 2px rgba(0, 212, 255, 0.2); +} + +/* Remove browser spin buttons */ +.credit-tool-window .ctw-credits-input::-webkit-inner-spin-button, +.credit-tool-window .ctw-credits-input::-webkit-outer-spin-button { + -webkit-appearance: none; + margin: 0; +} + +/* ── Colour helpers ──────────────────────────────────────── */ +.credit-tool-window .ctw-positive { + color: #3fb950; + font-weight: 700; +} + +.credit-tool-window .ctw-negative { + color: #f85149; + font-weight: 700; +} + +.credit-tool-window .ctw-greyed { + color: #7d8fa1; +} + +.credit-tool-window .ctw-debt { + color: #ff6b35; + font-weight: 700; +} + +/* ── Empty state ─────────────────────────────────────────── */ +.credit-tool-window .ctw-empty { + text-align: center; + padding: 18px 12px; + color: #7d8fa1; + font-size: 11px; + font-style: italic; +} + +/* ── Buttons ─────────────────────────────────────────────── */ +.credit-tool-window .ctw-btn { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 4px 10px; + border-radius: 3px; + border: 1px solid transparent; + font-family: "SLA Borg UI", "Rajdhani", monospace, sans-serif; + font-size: 10px; + font-weight: 700; + letter-spacing: 0.06em; + text-transform: uppercase; + cursor: pointer; + transition: all 0.15s ease; + white-space: nowrap; +} + +.credit-tool-window .ctw-btn-save { + background: rgba(0, 212, 255, 0.1); + border-color: rgba(0, 212, 255, 0.4); + color: #00d4ff; +} + +.credit-tool-window .ctw-btn-save:hover { + background: rgba(0, 212, 255, 0.22); + border-color: #00d4ff; + box-shadow: 0 0 6px rgba(0, 212, 255, 0.3); +} + +.credit-tool-window .ctw-btn-pay-all { + background: rgba(63, 185, 80, 0.12); + border-color: rgba(63, 185, 80, 0.5); + color: #3fb950; +} + +.credit-tool-window .ctw-btn-pay-all:hover { + background: rgba(63, 185, 80, 0.25); + border-color: #3fb950; + box-shadow: 0 0 6px rgba(63, 185, 80, 0.3); +} + +.credit-tool-window .ctw-btn-danger { + background: rgba(248, 81, 73, 0.1); + border-color: rgba(248, 81, 73, 0.4); + color: #f85149; +} + +.credit-tool-window .ctw-btn-danger:hover { + background: rgba(248, 81, 73, 0.22); + border-color: #f85149; + box-shadow: 0 0 6px rgba(248, 81, 73, 0.3); +} + +.credit-tool-window .ctw-btn:active { + transform: scale(0.96); +} + +/* ── Bulk panel ──────────────────────────────────────────── */ +.credit-tool-window .ctw-bulk-panel { + background: rgba(0, 212, 255, 0.02); +} + +.credit-tool-window .ctw-bulk-controls { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 10px; +} + +.credit-tool-window .ctw-label { + font-size: 10px; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; + color: #7d8fa1; + white-space: nowrap; +} + +.credit-tool-window .ctw-bulk-amount { + width: 80px; +} + +.credit-tool-window .ctw-radio-group { + display: flex; + align-items: center; + gap: 12px; + flex-wrap: wrap; +} + +.credit-tool-window .ctw-radio-label { + display: inline-flex; + align-items: center; + gap: 4px; + font-size: 10px; + font-weight: 600; + color: #c9d1d9; + cursor: pointer; + letter-spacing: 0.04em; + white-space: nowrap; +} + +.credit-tool-window .ctw-radio-label input[type="radio"] { + accent-color: #ff6b35; + width: 12px; + height: 12px; + cursor: pointer; +} + +.credit-tool-window .ctw-radio-label:hover { + color: #ff6b35; +} + +.credit-tool-window .ctw-bulk-buttons { + display: flex; + gap: 8px; + flex-wrap: wrap; + margin-left: auto; +} + +/* ── Scrollable tbody if many actors ─────────────────────── */ +.credit-tool-window .ctw-table tbody { + display: table-row-group; +} + +.credit-tool-window.credit-tool-window .window-content { + overflow-y: auto; +} + +/* ── 📒 SHIFT LEDGER TOOL ──────────────────────────────────────────────────── */ +/* ═══════════════════════════════════════════════════════════════════════════ + SLA Shift Ledger — temp_ledger.css + All rules scoped to .shift-ledger-window + ═══════════════════════════════════════════════════════════════════════════ */ + +.shift-ledger-window .window-content { + padding: 0; + background: #0d0d1a; + color: #c8d0e0; + font-family: 'Signika', 'Palatino Linotype', serif; +} + +/* ── Root container ────────────────────────────────────────────────────────── */ +.shift-ledger-window .slw-root { + display: flex; + flex-direction: column; + gap: 0; + min-height: 100%; +} + +/* ── Shared colour utilities ───────────────────────────────────────────────── */ +.shift-ledger-window .slw-positive { color: #44cc66; } +.shift-ledger-window .slw-negative { color: #cc2222; } +.shift-ledger-window .slw-debt { color: #ff6b35; } +.shift-ledger-window .slw-credits { color: #00d4ff; } +.shift-ledger-window .slw-neutral { color: #888; } +.shift-ledger-window .slw-mono { font-family: 'Courier New', Courier, monospace; } + +/* ── Summary bar ───────────────────────────────────────────────────────────── */ +.shift-ledger-window .slw-summary-bar { + display: flex; + flex-wrap: wrap; + gap: 0; + background: linear-gradient(135deg, #16213e 0%, #1a1a2e 100%); + border-bottom: 2px solid #00d4ff44; + padding: 10px 14px; +} + +.shift-ledger-window .slw-summary-item { + flex: 1 1 120px; + display: flex; + flex-direction: column; + align-items: center; + padding: 6px 10px; + border-right: 1px solid #ffffff12; +} + +.shift-ledger-window .slw-summary-item:last-child { + border-right: none; +} + +.shift-ledger-window .slw-summary-label { + font-size: 0.68em; + text-transform: uppercase; + letter-spacing: 0.08em; + color: #8899aa; + margin-bottom: 3px; + white-space: nowrap; +} + +.shift-ledger-window .slw-summary-value { + font-family: 'Courier New', Courier, monospace; + font-size: 1.25em; + font-weight: bold; +} + +/* ── PC list container ─────────────────────────────────────────────────────── */ +.shift-ledger-window .slw-pc-list { + display: flex; + flex-direction: column; + gap: 0; + padding: 10px 10px 4px 10px; +} + +.shift-ledger-window .slw-empty { + color: #667; + font-style: italic; + text-align: center; + padding: 20px; +} + +/* ── Individual PC card ────────────────────────────────────────────────────── */ +.shift-ledger-window .slw-pc-card { + background: #12122a; + border: 1px solid #2a2a4a; + border-left: 3px solid #00d4ff55; + border-radius: 4px; + margin-bottom: 8px; + overflow: hidden; + transition: border-left-color 0.15s ease; +} + +.shift-ledger-window .slw-pc-card:hover { + border-left-color: #00d4ff; +} + +/* PC card header */ +.shift-ledger-window .slw-pc-header { + display: flex; + align-items: center; + gap: 10px; + padding: 7px 12px; + background: linear-gradient(90deg, #1a1a3a 0%, #16213e 100%); + border-bottom: 1px solid #2a2a4a; +} + +.shift-ledger-window .slw-pc-name { + flex: 1; + font-size: 1em; + font-weight: bold; + color: #e0e8f0; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.shift-ledger-window .slw-scl-badge { + background: #ff6b3522; + color: #ff6b35; + border: 1px solid #ff6b3566; + border-radius: 3px; + font-size: 0.72em; + font-weight: bold; + padding: 2px 7px; + letter-spacing: 0.06em; + white-space: nowrap; +} + +.shift-ledger-window .slw-pc-credits { + font-family: 'Courier New', Courier, monospace; + font-size: 1.15em; + font-weight: bold; + color: #00d4ff; + white-space: nowrap; +} + +/* PC card body — three columns */ +.shift-ledger-window .slw-pc-body { + display: flex; + gap: 0; + padding: 8px 0 10px 0; +} + +.shift-ledger-window .slw-finance-col { + flex: 1; + padding: 0 12px; + border-right: 1px solid #1e1e3a; +} + +.shift-ledger-window .slw-finance-col:last-child { + border-right: none; +} + +.shift-ledger-window .slw-finance-col--summary { + flex: 0 0 160px; + display: flex; + flex-direction: column; + gap: 4px; +} + +.shift-ledger-window .slw-col-title { + font-size: 0.7em; + text-transform: uppercase; + letter-spacing: 0.1em; + color: #667788; + margin-bottom: 5px; + padding-bottom: 3px; + border-bottom: 1px solid #1e2a3a; +} + +/* Finance rows */ +.shift-ledger-window .slw-finance-row { + display: flex; + justify-content: space-between; + align-items: baseline; + padding: 1px 0; + font-size: 0.8em; +} + +.shift-ledger-window .slw-finance-label { + color: #8899aa; + white-space: nowrap; + margin-right: 8px; +} + +.shift-ledger-window .slw-finance-val { + text-align: right; + min-width: 52px; + color: #b0bcc8; +} + +/* Total row emphasis */ +.shift-ledger-window .slw-total-row { + border-top: 1px solid #2a2a4a; + margin-top: 3px; + padding-top: 3px; + font-weight: bold; +} + +.shift-ledger-window .slw-total-row .slw-finance-label { + color: #aabbc8; +} + +/* ── Buttons ───────────────────────────────────────────────────────────────── */ +.shift-ledger-window .slw-btn { + display: inline-block; + padding: 5px 10px; + font-size: 0.78em; + font-family: 'Signika', sans-serif; + font-weight: bold; + letter-spacing: 0.04em; + border: none; + border-radius: 3px; + cursor: pointer; + transition: filter 0.12s ease, transform 0.08s ease; + white-space: nowrap; +} + +.shift-ledger-window .slw-btn:hover { + filter: brightness(1.2); + transform: translateY(-1px); +} + +.shift-ledger-window .slw-btn:active { + transform: translateY(0); + filter: brightness(0.9); +} + +/* Per-PC process button */ +.shift-ledger-window .slw-btn--process { + background: linear-gradient(135deg, #1a3a2a 0%, #224433 100%); + color: #44cc66; + border: 1px solid #44cc6655; + margin-top: 8px; + width: 100%; +} + +.shift-ledger-window .slw-btn--process:hover { + background: linear-gradient(135deg, #224433 0%, #2a5540 100%); + border-color: #44cc66aa; +} + +/* ── Action bar (bottom) ───────────────────────────────────────────────────── */ +.shift-ledger-window .slw-action-bar { + display: flex; + justify-content: flex-end; + gap: 8px; + padding: 8px 12px 10px 12px; + background: #0d0d1a; + border-top: 1px solid #1e1e3a; +} + +/* Process all */ +.shift-ledger-window .slw-btn--all { + background: linear-gradient(135deg, #1a2a3a 0%, #162030 100%); + color: #00d4ff; + border: 1px solid #00d4ff55; +} + +.shift-ledger-window .slw-btn--all:hover { + background: linear-gradient(135deg, #1e3040 0%, #1a2838 100%); + border-color: #00d4ffaa; +} + +/* Export to chat */ +.shift-ledger-window .slw-btn--export { + background: linear-gradient(135deg, #2a1a2a 0%, #221422 100%); + color: #cc88ff; + border: 1px solid #cc88ff55; +} + +.shift-ledger-window .slw-btn--export:hover { + background: linear-gradient(135deg, #311e31 0%, #291828 100%); + border-color: #cc88ffaa; +} + +/* ── Scrollbar theming ─────────────────────────────────────────────────────── */ +.shift-ledger-window .window-content::-webkit-scrollbar { + width: 6px; +} + +.shift-ledger-window .window-content::-webkit-scrollbar-track { + background: #0d0d1a; +} + +.shift-ledger-window .window-content::-webkit-scrollbar-thumb { + background: #2a2a4a; + border-radius: 3px; +} + +.shift-ledger-window .window-content::-webkit-scrollbar-thumb:hover { + background: #00d4ff44; +} + +/* ── ☠ NPC THREAT BOARD ──────────────────────────────────────────────────── */ +/* ────────────────────────────────────────────────────────────────────────────── + SLANPCThreatBoard — dark sci-fi NPC tactical overview + All rules scoped to .npc-threat-board + ────────────────────────────────────────────────────────────────────────────── */ + +/* ── Window shell ─────────────────────────────────────────────────────────── */ +.npc-threat-board { + background: #0d0d1a; + color: #c8d0e0; + font-family: 'Helvetica Neue', Arial, sans-serif; + font-size: 12px; +} + +.npc-threat-board .window-content { + background: #0d0d1a; + padding: 0; + overflow: hidden; + display: flex; + flex-direction: column; +} + +/* ── Outer wrapper ────────────────────────────────────────────────────────── */ +.npc-threat-board .ntb-wrapper { + display: flex; + flex-direction: column; + height: 100%; + overflow: hidden; +} + +/* ── Filter bar ───────────────────────────────────────────────────────────── */ +.npc-threat-board .ntb-filterbar { + display: flex; + flex-wrap: wrap; + gap: 4px 10px; + align-items: center; + padding: 6px 8px; + background: #111126; + border-bottom: 1px solid #2a2a4a; + flex-shrink: 0; +} + +.npc-threat-board .ntb-filter-group { + display: flex; + align-items: center; + gap: 3px; +} + +.npc-threat-board .ntb-sort-label { + color: #6a7090; + font-size: 11px; + margin-right: 2px; +} + +.npc-threat-board .ntb-filter-btn, +.npc-threat-board .ntb-threat-filter, +.npc-threat-board .ntb-sort-btn { + background: #1a1a2e; + border: 1px solid #2e2e50; + color: #8890aa; + border-radius: 3px; + padding: 2px 7px; + font-size: 11px; + cursor: pointer; + transition: background 0.15s, color 0.15s, border-color 0.15s; + white-space: nowrap; + line-height: 1.6; +} + +.npc-threat-board .ntb-filter-btn:hover, +.npc-threat-board .ntb-threat-filter:hover, +.npc-threat-board .ntb-sort-btn:hover { + background: #22224a; + color: #c8d0e0; + border-color: #4a4a88; +} + +.npc-threat-board .ntb-filter-btn.ntb-active, +.npc-threat-board .ntb-threat-filter.ntb-active, +.npc-threat-board .ntb-sort-btn.ntb-active { + background: #2a2a5c; + color: #e0e8ff; + border-color: #5555aa; +} + +/* Active threat filters — color-coded active state */ +.npc-threat-board .ntb-threat-filter[data-threat="low"].ntb-active { background: #1a3a22; border-color: #44cc66; color: #7deeaa; } +.npc-threat-board .ntb-threat-filter[data-threat="med"].ntb-active { background: #3a2e10; border-color: #ffaa00; color: #ffd055; } +.npc-threat-board .ntb-threat-filter[data-threat="high"].ntb-active { background: #3a1212; border-color: #cc2222; color: #ff6666; } +.npc-threat-board .ntb-threat-filter[data-threat="elite"].ntb-active{ background: #2a1040; border-color: #9933cc; color: #cc77ff; } + +/* Dead toggle active */ +.npc-threat-board .ntb-toggle-dead.ntb-active { + background: #2a1010; + border-color: #882222; + color: #ff8888; +} + +/* ── Scrollable card grid ─────────────────────────────────────────────────── */ +.npc-threat-board .ntb-grid { + display: flex; + flex-wrap: wrap; + gap: 8px; + padding: 8px; + overflow-y: auto; + flex: 1 1 0; + align-content: flex-start; +} + +/* Thin scrollbar */ +.npc-threat-board .ntb-grid::-webkit-scrollbar { width: 6px; } +.npc-threat-board .ntb-grid::-webkit-scrollbar-track { background: #0d0d1a; } +.npc-threat-board .ntb-grid::-webkit-scrollbar-thumb { background: #2a2a4a; border-radius: 3px; } +.npc-threat-board .ntb-grid::-webkit-scrollbar-thumb:hover { background: #4a4a88; } + +/* ── Empty state ──────────────────────────────────────────────────────────── */ +.npc-threat-board .ntb-empty { + width: 100%; + text-align: center; + padding: 40px 20px; + color: #4a5070; + font-style: italic; + font-size: 13px; +} + +/* ── NPC Card ─────────────────────────────────────────────────────────────── */ +.npc-threat-board .ntb-card { + position: relative; + background: #13132a; + border: 1px solid #2a2a4a; + border-radius: 5px; + padding: 7px 8px 6px; + width: 175px; + flex-shrink: 0; + cursor: pointer; + transition: background 0.15s, border-color 0.2s, transform 0.1s; + overflow: hidden; +} + +.npc-threat-board .ntb-card:hover { + background: #1a1a38; + border-color: #4a4a88; + transform: translateY(-1px); +} + +/* Card threat border accent — left bar */ +.npc-threat-board .ntb-card::before { + content: ''; + position: absolute; + left: 0; top: 0; bottom: 0; + width: 3px; + border-radius: 5px 0 0 5px; +} +.npc-threat-board .ntb-card.threat-low::before { background: #44cc66; } +.npc-threat-board .ntb-card.threat-med::before { background: #ffaa00; } +.npc-threat-board .ntb-card.threat-high::before { background: #cc3333; } +.npc-threat-board .ntb-card.threat-elite::before { background: #9933cc; } + +/* Dead card dimming */ +.npc-threat-board .ntb-card.ntb-dead { + opacity: 0.55; + filter: grayscale(60%); +} + +/* Dead skull overlay */ +.npc-threat-board .ntb-skull-overlay { + position: absolute; + top: 4px; right: 5px; + font-size: 16px; + pointer-events: none; + opacity: 0.8; + z-index: 2; +} + +/* ── Card header: portrait + name + threat badge ──────────────────────────── */ +.npc-threat-board .ntb-card-header { + display: flex; + align-items: flex-start; + gap: 7px; + margin-bottom: 6px; +} + +.npc-threat-board .ntb-portrait-wrap { + flex-shrink: 0; + width: 40px; + height: 40px; + border-radius: 50%; + border: 2px solid #2a2a4a; + overflow: hidden; +} + +/* Portrait border color mirrors threat */ +.npc-threat-board .ntb-border-threat-low { border-color: #44cc66; } +.npc-threat-board .ntb-border-threat-med { border-color: #ffaa00; } +.npc-threat-board .ntb-border-threat-high { border-color: #cc3333; } +.npc-threat-board .ntb-border-threat-elite { border-color: #9933cc; } + +.npc-threat-board .ntb-portrait { + width: 100%; + height: 100%; + object-fit: cover; + display: block; +} + +.npc-threat-board .ntb-name-block { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + gap: 4px; + padding-top: 1px; +} + +.npc-threat-board .ntb-name { + font-weight: bold; + font-size: 12px; + color: #d8e0f0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + line-height: 1.2; +} + +/* Scene indicator badge */ +.npc-threat-board .ntb-scene-badge { + display: inline-block; + margin-left: 4px; + color: #44aaff; + font-size: 10px; + vertical-align: middle; +} + +/* ── Threat badge ─────────────────────────────────────────────────────────── */ +.npc-threat-board .ntb-threat-badge { + display: inline-block; + font-size: 10px; + font-weight: bold; + padding: 1px 5px; + border-radius: 3px; + letter-spacing: 0.04em; + text-transform: uppercase; + line-height: 1.5; + align-self: flex-start; +} + +.npc-threat-board .ntb-threat-badge.threat-low { background: #1a3a22; color: #7deeaa; border: 1px solid #44cc66; } +.npc-threat-board .ntb-threat-badge.threat-med { background: #3a2e10; color: #ffd055; border: 1px solid #ffaa00; } +.npc-threat-board .ntb-threat-badge.threat-high { background: #3a1212; color: #ff7070; border: 1px solid #cc3333; } +.npc-threat-board .ntb-threat-badge.threat-elite { background: #2a1040; color: #cc77ff; border: 1px solid #9933cc; } + +/* ── HP bar + controls ────────────────────────────────────────────────────── */ +.npc-threat-board .ntb-hp-row { + margin-bottom: 5px; +} + +.npc-threat-board .ntb-hp-track { + width: 100%; + height: 6px; + background: #0a0a18; + border-radius: 3px; + overflow: hidden; + margin-bottom: 3px; + border: 1px solid #1e1e38; +} + +.npc-threat-board .ntb-hp-fill { + height: 100%; + border-radius: 3px; + transition: width 0.3s ease, background 0.3s ease; + min-width: 1px; +} + +.npc-threat-board .ntb-hp-controls { + display: flex; + align-items: center; + gap: 4px; + justify-content: center; +} + +.npc-threat-board .ntb-hp-text { + font-size: 11px; + color: #9090b8; + min-width: 40px; + text-align: center; + flex: 1; +} + +.npc-threat-board .ntb-hp-btn { + background: #1c1c38; + border: 1px solid #2e2e50; + color: #8890aa; + border-radius: 3px; + width: 18px; + height: 18px; + font-size: 14px; + line-height: 1; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + padding: 0; + transition: background 0.15s, color 0.15s; +} + +.npc-threat-board .ntb-hp-btn:hover { + background: #28284e; + color: #d0d8ee; + border-color: #5555aa; +} + +.npc-threat-board .ntb-hp-dec:hover { color: #ff6666; border-color: #882222; } +.npc-threat-board .ntb-hp-inc:hover { color: #66dd88; border-color: #226644; } + +/* ── Attribute mini-grid ──────────────────────────────────────────────────── */ +.npc-threat-board .ntb-attrs { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 3px; + margin-bottom: 5px; + background: #0f0f22; + border-radius: 3px; + padding: 4px 2px; + border: 1px solid #1e1e38; +} + +.npc-threat-board .ntb-attr { + display: flex; + flex-direction: column; + align-items: center; + gap: 1px; +} + +.npc-threat-board .ntb-attr-label { + font-size: 9px; + color: #5a6080; + text-transform: uppercase; + letter-spacing: 0.06em; + line-height: 1; +} + +.npc-threat-board .ntb-attr-val { + font-size: 13px; + font-weight: bold; + color: #b8c8e0; + line-height: 1.1; +} + +/* ── Damage / Armor badges + status dots ──────────────────────────────────── */ +.npc-threat-board .ntb-badges { + display: flex; + align-items: center; + gap: 5px; + flex-wrap: wrap; +} + +.npc-threat-board .ntb-badge { + display: inline-block; + font-size: 10px; + padding: 1px 5px; + border-radius: 3px; + line-height: 1.5; + white-space: nowrap; +} + +.npc-threat-board .ntb-badge-dmg { + background: #2a1010; + color: #ff9090; + border: 1px solid #551111; +} + +.npc-threat-board .ntb-badge-armor { + background: #10202a; + color: #70bbee; + border: 1px solid #11334a; +} + +.npc-threat-board .ntb-statuses { + display: flex; + flex-wrap: wrap; + gap: 3px; + align-items: center; +} + +.npc-threat-board .ntb-status-dot { + display: inline-block; + width: 8px; + height: 8px; + border-radius: 50%; + background: #aa8800; + border: 1px solid #ffcc44; + cursor: default; +} + +/* ── 📋 BPN TRACKER ──────────────────────────────────────────────────── */ +/* ============================================================ + BPN Tracker — scoped to .bpn-tracker-window + Dark sci-fi aesthetic, SLA Industries + ============================================================ */ + +.bpn-tracker-window .window-content { + background: #0f1117; + color: #c8cdd8; + font-family: 'Roboto', 'Arial', sans-serif; + font-size: 13px; + padding: 0; + overflow-y: auto; +} + +/* ── Inner wrapper ─────────────────────────────────────────── */ +.bpn-tracker-window .bpn-tracker-inner { + display: flex; + flex-direction: column; + gap: 12px; + padding: 12px; +} + +/* ── Empty state ───────────────────────────────────────────── */ +.bpn-tracker-window .bpn-empty-state { + color: #555; + font-style: italic; + text-align: center; + padding: 20px 0; +} + +/* ── Mission card ──────────────────────────────────────────── */ +.bpn-tracker-window .bpn-mission-card { + background: #14181f; + border: 1px solid #2a2d35; + border-left: 4px solid #3399ff; /* overridden inline per type */ + border-radius: 4px; + padding: 10px 12px; + display: flex; + flex-direction: column; + gap: 8px; +} + +/* Card header row */ +.bpn-tracker-window .bpn-card-header { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; +} + +/* BPN code — monospace, prominent */ +.bpn-tracker-window .bpn-code { + font-family: 'Courier New', Courier, monospace; + font-size: 14px; + font-weight: 700; + color: #e0e6f0; + letter-spacing: 0.04em; + flex-shrink: 0; +} + +/* Type badge */ +.bpn-tracker-window .bpn-type-badge { + font-size: 11px; + font-weight: 600; + padding: 2px 7px; + border-radius: 3px; + border: 1px solid; + letter-spacing: 0.05em; + text-transform: uppercase; + flex-shrink: 0; +} + +/* Status badge */ +.bpn-tracker-window .bpn-status-badge { + font-size: 11px; + font-weight: 600; + padding: 2px 7px; + border-radius: 3px; + letter-spacing: 0.04em; + text-transform: uppercase; + flex-shrink: 0; +} + +.bpn-tracker-window .bpn-status-pending { background: #2a2a2a; color: #888; border: 1px solid #555; } +.bpn-tracker-window .bpn-status-active { background: #0d2a2a; color: #00e5e5; border: 1px solid #00bfbf; } +.bpn-tracker-window .bpn-status-complete { background: #0d2a1a; color: #44dd66; border: 1px solid #33aa44; } +.bpn-tracker-window .bpn-status-failed { background: #2a0d0d; color: #ff5555; border: 1px solid #cc2222; } + +/* Clear button — right-aligned */ +.bpn-tracker-window .bpn-clear-btn { + margin-left: auto; + background: transparent; + border: 1px solid #443333; + color: #885555; + font-size: 11px; + padding: 2px 8px; + border-radius: 3px; + cursor: pointer; + transition: background 0.15s, color 0.15s; + flex-shrink: 0; +} +.bpn-tracker-window .bpn-clear-btn:hover { + background: #3a1a1a; + color: #ff6666; + border-color: #cc2222; +} + +/* ── Card rows ─────────────────────────────────────────────── */ +.bpn-tracker-window .bpn-card-row { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; +} + +.bpn-tracker-window .bpn-label { + font-size: 11px; + color: #666; + text-transform: uppercase; + letter-spacing: 0.06em; + flex-shrink: 0; +} + +.bpn-tracker-window .bpn-label-type { + margin-left: 8px; +} + +.bpn-tracker-window .bpn-status-select, +.bpn-tracker-window .bpn-type-select { + background: #1a1e26; + border: 1px solid #2e323c; + color: #c8cdd8; + font-size: 12px; + padding: 3px 6px; + border-radius: 3px; + cursor: pointer; +} +.bpn-tracker-window .bpn-status-select:focus, +.bpn-tracker-window .bpn-type-select:focus { + outline: none; + border-color: #3399ff; +} + +/* Reward row */ +.bpn-tracker-window .bpn-reward-row { + align-items: center; +} + +.bpn-tracker-window .bpn-reward-label { + font-size: 12px; + color: #8899aa; +} +.bpn-tracker-window .bpn-reward-label strong { + color: #c8b46e; + font-family: 'Courier New', Courier, monospace; +} + +/* Pay Out button — gold/amber */ +.bpn-tracker-window .bpn-payout-btn { + background: #3a2a00; + border: 1px solid #8a6a00; + color: #ffc233; + font-size: 11px; + font-weight: 600; + padding: 3px 10px; + border-radius: 3px; + cursor: pointer; + letter-spacing: 0.04em; + transition: background 0.15s; + margin-left: auto; +} +.bpn-tracker-window .bpn-payout-btn:hover { + background: #5a4400; + border-color: #ccaa00; + color: #ffe066; +} + +/* ── Description / Objectives fields ──────────────────────── */ +.bpn-tracker-window .bpn-card-field { + display: flex; + flex-direction: column; + gap: 3px; +} + +.bpn-tracker-window .bpn-textarea { + background: #0d1014; + border: 1px solid #252830; + color: #b0b8c8; + font-family: 'Courier New', Courier, monospace; + font-size: 12px; + line-height: 1.5; + padding: 6px 8px; + border-radius: 3px; + resize: vertical; + width: 100%; + box-sizing: border-box; + transition: border-color 0.15s; +} +.bpn-tracker-window .bpn-textarea:focus { + outline: none; + border-color: #3399ff; + background: #0f1318; +} +.bpn-tracker-window .bpn-textarea::placeholder { + color: #3a3f4a; +} + +/* ── Card footer ───────────────────────────────────────────── */ +.bpn-tracker-window .bpn-card-footer { + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + gap: 8px; + padding-top: 4px; + border-top: 1px solid #1e2228; +} + +/* PC badge strip */ +.bpn-tracker-window .bpn-pc-row { + display: flex; + flex-wrap: wrap; + gap: 5px; +} + +.bpn-tracker-window .bpn-pc-badge { + background: #1a2030; + border: 1px solid #2e3a50; + color: #7899cc; + font-size: 11px; + padding: 2px 8px; + border-radius: 10px; + letter-spacing: 0.03em; +} + +.bpn-tracker-window .bpn-pc-badge-unassigned { + background: #1a1a1a; + border-color: #333; + color: #555; +} + +/* Save button */ +.bpn-tracker-window .bpn-save-btn { + background: #0d2233; + border: 1px solid #1a5080; + color: #55aadd; + font-size: 11px; + font-weight: 600; + padding: 3px 12px; + border-radius: 3px; + cursor: pointer; + letter-spacing: 0.03em; + transition: background 0.15s; + flex-shrink: 0; +} +.bpn-tracker-window .bpn-save-btn:hover { + background: #163348; + border-color: #3399ff; + color: #88ccff; +} + +/* ── Unassigned section ────────────────────────────────────── */ +.bpn-tracker-window .bpn-unassigned-section { + background: #111318; + border: 1px dashed #2a2a2a; + border-radius: 4px; + padding: 8px 12px; + display: flex; + flex-direction: column; + gap: 6px; +} + +.bpn-tracker-window .bpn-unassigned-label { + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.07em; + color: #444; +} + +/* ── Assign BPN Panel ──────────────────────────────────────── */ +.bpn-tracker-window .bpn-assign-panel { + background: #111520; + border: 1px solid #1e2535; + border-radius: 4px; + overflow: hidden; +} + +.bpn-tracker-window .bpn-assign-header { + background: #151a28; + border-bottom: 1px solid #1e2535; + color: #6688bb; + font-size: 11px; + font-weight: 700; + letter-spacing: 0.09em; + text-transform: uppercase; + padding: 7px 12px; +} + +.bpn-tracker-window .bpn-assign-body { + display: flex; + gap: 12px; + padding: 10px 12px; +} + +/* PC checklist — left column */ +.bpn-tracker-window .bpn-assign-pcs { + min-width: 130px; + flex-shrink: 0; + display: flex; + flex-direction: column; + gap: 4px; +} + +.bpn-tracker-window .bpn-pc-checklist { + display: flex; + flex-direction: column; + gap: 4px; + max-height: 160px; + overflow-y: auto; + padding-right: 4px; +} + +.bpn-tracker-window .bpn-pc-check { + display: flex; + align-items: center; + gap: 6px; + font-size: 12px; + color: #8899aa; + cursor: pointer; +} +.bpn-tracker-window .bpn-pc-check:hover { + color: #aabbcc; +} +.bpn-tracker-window .bpn-pc-check input[type="checkbox"] { + accent-color: #3399ff; + cursor: pointer; +} + +/* Fields — right column */ +.bpn-tracker-window .bpn-assign-fields { + flex: 1; + display: flex; + flex-direction: column; + gap: 7px; +} + +.bpn-tracker-window .bpn-assign-row { + display: flex; + align-items: center; + gap: 7px; + flex-wrap: wrap; +} + +.bpn-tracker-window .bpn-assign-code, +.bpn-tracker-window .bpn-assign-reward { + background: #0d1014; + border: 1px solid #252830; + color: #c8cdd8; + font-family: 'Courier New', Courier, monospace; + font-size: 12px; + padding: 4px 8px; + border-radius: 3px; + flex: 1; + min-width: 80px; +} +.bpn-tracker-window .bpn-assign-code:focus, +.bpn-tracker-window .bpn-assign-reward:focus { + outline: none; + border-color: #3399ff; +} + +.bpn-tracker-window .bpn-assign-type, +.bpn-tracker-window .bpn-assign-status { + background: #1a1e26; + border: 1px solid #2e323c; + color: #c8cdd8; + font-size: 12px; + padding: 3px 6px; + border-radius: 3px; + cursor: pointer; + flex: 1; + min-width: 70px; +} +.bpn-tracker-window .bpn-assign-type:focus, +.bpn-tracker-window .bpn-assign-status:focus { + outline: none; + border-color: #3399ff; +} + +.bpn-tracker-window .bpn-assign-submit-row { + justify-content: flex-end; + padding-top: 4px; + border-top: 1px solid #1e2228; + margin-top: 2px; +} + +/* Assign BPN submit button */ +.bpn-tracker-window .bpn-assign-btn { + background: #0d2a18; + border: 1px solid #1a6640; + color: #44cc88; + font-size: 12px; + font-weight: 700; + letter-spacing: 0.05em; + padding: 5px 16px; + border-radius: 3px; + cursor: pointer; + text-transform: uppercase; + transition: background 0.15s; +} +.bpn-tracker-window .bpn-assign-btn:hover { + background: #164030; + border-color: #33cc77; + color: #88ffbb; +} + +/* ── Scrollbar styling (webkit) ────────────────────────────── */ +.bpn-tracker-window .window-content::-webkit-scrollbar, +.bpn-tracker-window .bpn-pc-checklist::-webkit-scrollbar { + width: 5px; +} +.bpn-tracker-window .window-content::-webkit-scrollbar-track, +.bpn-tracker-window .bpn-pc-checklist::-webkit-scrollbar-track { + background: #0a0c10; +} +.bpn-tracker-window .window-content::-webkit-scrollbar-thumb, +.bpn-tracker-window .bpn-pc-checklist::-webkit-scrollbar-thumb { + background: #2a3040; + border-radius: 3px; +} +.bpn-tracker-window .window-content::-webkit-scrollbar-thumb:hover, +.bpn-tracker-window .bpn-pc-checklist::-webkit-scrollbar-thumb:hover { + background: #3a4455; +} + +/* ── ⚡ CONDITION APPLICATOR ──────────────────────────────────────────────────── */ +/* ============================================================ + SLA Condition Applicator — temp_condapp.css + Scoped to .condition-applicator-window + ============================================================ */ + +.condition-applicator-window .window-content { + padding: 0; + background: #0d0d1a; + color: #c8d0e0; + font-family: 'Roboto Condensed', 'Arial Narrow', Arial, sans-serif; + overflow: hidden; + display: flex; + flex-direction: column; +} + +/* ---- Wrapper ---- */ +.condition-applicator-window .cap-wrapper { + display: flex; + flex-direction: column; + height: 100%; + overflow: hidden; +} + +/* ============================================================ + HEADER + ============================================================ */ +.condition-applicator-window .cap-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 8px 12px; + background: #13132b; + border-bottom: 1px solid #00d4ff33; + flex-shrink: 0; + gap: 8px; +} + +.condition-applicator-window .cap-header-info { + display: flex; + align-items: center; + gap: 6px; + min-width: 0; +} + +.condition-applicator-window .cap-header-label { + font-size: 0.7rem; + text-transform: uppercase; + letter-spacing: 0.08em; + color: #6a7a9a; +} + +.condition-applicator-window .cap-selected-name { + font-size: 0.95rem; + font-weight: 700; + color: #00d4ff; + text-shadow: 0 0 8px #00d4ff88; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 180px; +} + +.condition-applicator-window .cap-header-actions { + display: flex; + gap: 6px; + flex-shrink: 0; +} + +/* ---- Buttons ---- */ +.condition-applicator-window .cap-btn { + display: inline-flex; + align-items: center; + gap: 5px; + padding: 4px 10px; + font-size: 0.75rem; + font-weight: 600; + border: 1px solid #2a2a4a; + border-radius: 3px; + background: #1a1a2e; + color: #8a9bbf; + cursor: pointer; + transition: background 0.15s, border-color 0.15s, color 0.15s; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.condition-applicator-window .cap-btn:hover { + background: #22224a; + border-color: #00d4ff55; + color: #c8d0e0; +} + +.condition-applicator-window .cap-btn--toggle.cap-btn--active { + background: #0a2a3a; + border-color: #00d4ff; + color: #00d4ff; + box-shadow: 0 0 6px #00d4ff44; +} + +.condition-applicator-window .cap-btn--danger { + border-color: #3a1a1a; + color: #c05050; +} + +.condition-applicator-window .cap-btn--danger:hover { + background: #2a0f0f; + border-color: #ff4444; + color: #ff6666; +} + +/* ============================================================ + BODY — two-column layout + ============================================================ */ +.condition-applicator-window .cap-body { + display: flex; + flex: 1 1 0; + overflow: hidden; +} + +/* ============================================================ + LEFT PANEL — PC Roster (30%) + ============================================================ */ +.condition-applicator-window .cap-pc-list { + width: 30%; + min-width: 140px; + display: flex; + flex-direction: column; + border-right: 1px solid #1e1e3a; + background: #0a0a18; + flex-shrink: 0; +} + +.condition-applicator-window .cap-pc-list-header { + padding: 6px 10px; + font-size: 0.65rem; + text-transform: uppercase; + letter-spacing: 0.1em; + color: #4a5a7a; + background: #0d0d22; + border-bottom: 1px solid #1a1a30; + flex-shrink: 0; +} + +.condition-applicator-window .cap-pc-list-scroll { + overflow-y: auto; + flex: 1 1 0; + scrollbar-width: thin; + scrollbar-color: #1e2a4a #0a0a18; +} + +.condition-applicator-window .cap-pc-list-scroll::-webkit-scrollbar { + width: 4px; +} +.condition-applicator-window .cap-pc-list-scroll::-webkit-scrollbar-track { + background: #0a0a18; +} +.condition-applicator-window .cap-pc-list-scroll::-webkit-scrollbar-thumb { + background: #1e2a4a; + border-radius: 2px; +} + +/* PC Row */ +.condition-applicator-window .cap-pc-row { + display: flex; + align-items: center; + justify-content: space-between; + padding: 7px 10px; + cursor: pointer; + border-bottom: 1px solid #111128; + border-left: 3px solid transparent; + transition: background 0.12s, border-left-color 0.12s; + gap: 4px; +} + +.condition-applicator-window .cap-pc-row:hover { + background: #12122a; + border-left-color: #3a3a6a; +} + +.condition-applicator-window .cap-pc-row--selected { + background: #141430; + border-left-color: #ff8800; +} + +.condition-applicator-window .cap-pc-row--selected .cap-pc-name { + color: #ffaa44; +} + +.condition-applicator-window .cap-pc-name { + font-size: 0.82rem; + font-weight: 600; + color: #a8b8d8; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + flex: 1 1 0; +} + +.condition-applicator-window .cap-pc-dots { + display: flex; + gap: 3px; + flex-wrap: wrap; + justify-content: flex-end; + flex-shrink: 0; + max-width: 60px; +} + +.condition-applicator-window .cap-cond-dot { + display: inline-flex; + align-items: center; + justify-content: center; + width: 14px; + height: 14px; + border-radius: 50%; + background: #00d4ff22; + border: 1px solid #00d4ff66; + font-size: 0.45rem; + color: #00d4ff; +} + +.condition-applicator-window .cap-no-pcs { + padding: 12px 10px; + font-size: 0.75rem; + color: #4a5a7a; + font-style: italic; +} + +/* ============================================================ + RIGHT PANEL — Condition Grid (70%) + ============================================================ */ +.condition-applicator-window .cap-cond-panel { + flex: 1 1 0; + overflow-y: auto; + padding: 8px; + background: #0d0d1a; + scrollbar-width: thin; + scrollbar-color: #1e2a4a #0d0d1a; +} + +.condition-applicator-window .cap-cond-panel::-webkit-scrollbar { + width: 4px; +} +.condition-applicator-window .cap-cond-panel::-webkit-scrollbar-track { + background: #0d0d1a; +} +.condition-applicator-window .cap-cond-panel::-webkit-scrollbar-thumb { + background: #1e2a4a; + border-radius: 2px; +} + +.condition-applicator-window .cap-cond-grid { + display: flex; + flex-wrap: wrap; + gap: 6px; + align-content: flex-start; +} + +/* Condition Tile */ +.condition-applicator-window .cap-cond-tile { + position: relative; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 80px; + height: 80px; + padding: 6px 4px 4px; + border-radius: 4px; + background: #111128; + border: 1px solid #1e1e3a; + cursor: pointer; + transition: background 0.15s, border-color 0.15s, box-shadow 0.15s; + gap: 4px; + overflow: hidden; + text-align: center; +} + +.condition-applicator-window .cap-cond-tile:hover { + background: #181838; + border-color: #3a3a7a; +} + +.condition-applicator-window .cap-cond-tile--active { + background: #062030; + border: 1px solid #00d4ff; + box-shadow: 0 0 8px #00d4ff44, inset 0 0 12px #00d4ff18; +} + +.condition-applicator-window .cap-cond-tile--active .cap-cond-icon, +.condition-applicator-window .cap-cond-tile--active .cap-cond-label { + color: #00d4ff; + text-shadow: 0 0 6px #00d4ff88; +} + +.condition-applicator-window .cap-cond-icon { + font-size: 1.4rem; + color: #6a7a9a; + line-height: 1; + transition: color 0.15s, text-shadow 0.15s; +} + +.condition-applicator-window .cap-cond-label { + font-size: 0.6rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.04em; + color: #7a8aaa; + line-height: 1.2; + word-break: break-word; + transition: color 0.15s, text-shadow 0.15s; +} + +/* Dice Modifier Badge */ +.condition-applicator-window .cap-mod-badge { + position: absolute; + top: 2px; + right: 2px; + font-size: 0.5rem; + font-weight: 700; + background: #8b0000; + color: #ffaaaa; + border-radius: 2px; + padding: 1px 3px; + line-height: 1.3; + white-space: nowrap; + border: 1px solid #cc222233; + text-transform: uppercase; + letter-spacing: 0.02em; +} + +.condition-applicator-window .cap-no-conds { + padding: 16px; + font-size: 0.8rem; + color: #4a5a7a; + font-style: italic; +} + +/* ============================================================ + STATUS BAR + ============================================================ */ +.condition-applicator-window .cap-statusbar { + display: flex; + align-items: center; + gap: 8px; + padding: 5px 10px; + background: #0a0a18; + border-top: 1px solid #1a1a30; + flex-shrink: 0; + min-height: 30px; + overflow: hidden; +} + +.condition-applicator-window .cap-statusbar-label { + font-size: 0.65rem; + text-transform: uppercase; + letter-spacing: 0.08em; + color: #4a5a7a; + flex-shrink: 0; +} + +.condition-applicator-window .cap-statusbar-tags { + display: flex; + flex-wrap: nowrap; + gap: 5px; + overflow-x: auto; + scrollbar-width: none; + align-items: center; +} + +.condition-applicator-window .cap-statusbar-tags::-webkit-scrollbar { + display: none; +} + +.condition-applicator-window .cap-status-tag { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 2px 7px; + border-radius: 10px; + background: #062030; + border: 1px solid #00d4ff55; + color: #00d4ff; + font-size: 0.65rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.04em; + white-space: nowrap; +} + +.condition-applicator-window .cap-status-none { + font-size: 0.7rem; + color: #3a4a6a; + font-style: italic; +} + +.condition-applicator-window .cap-status-mode { + font-size: 0.7rem; + color: #00d4ff88; + font-style: italic; +} diff --git a/system.json b/system.json new file mode 100644 index 0000000..9739ccc --- /dev/null +++ b/system.json @@ -0,0 +1,91 @@ +{ + "id": "slazero2", + "title": "SLA Zero Two", + "description": "SLA Zero Two \u2014 the sequel system for SLA Industries on the Zero Engine ruleset. All the mechanics of Zero Engine v1.7.6 with a full presentation overhaul: custom dice, redesigned sheets, and cinematic chat cards.", + "version": "2.0.0", + "compatibility": { + "minimum": 13, + "verified": "14.359" + }, + "authors": [ + { + "name": "slaguru666" + } + ], + "scripts": [], + "esmodules": [ + "slazero2.mjs" + ], + "styles": [ + "styles/slazero2.css" + ], + "languages": [ + { + "lang": "en", + "name": "English", + "path": "lang/en.json" + } + ], + "socket": false, + "initiative": "@combat.initiative", + "grid": { + "distance": 1, + "units": "m" + }, + "primaryTokenAttribute": "health", + "secondaryTokenAttribute": null, + "packs": [ + { + "name": "sla-armors", + "label": "SLA Armors", + "path": "packs/sla-armors", + "type": "Item", + "system": "slazero2" + }, + { + "name": "sla-drugs", + "label": "SLA Drugs & Consumables", + "path": "packs/sla-drugs", + "type": "Item", + "system": "slazero2" + }, + { + "name": "sla-ebb-formulae", + "label": "SLA Ebb Formulae", + "path": "packs/sla-ebb-formulae", + "type": "Item", + "system": "slazero2" + }, + { + "name": "sla-specialties", + "label": "SLA Specialties", + "path": "packs/sla-specialties", + "type": "Item", + "system": "slazero2" + }, + { + "name": "sla-weapons", + "label": "SLA Weapons", + "path": "packs/sla-weapons", + "type": "Item", + "system": "slazero2" + }, + { + "name": "sla-equipment", + "label": "SLA Equipment & Gear", + "path": "packs/sla-equipment", + "type": "Item", + "system": "slazero2" + }, + { + "name": "sla-ammo", + "label": "SLA Ammunition", + "path": "packs/sla-ammo", + "type": "Item", + "system": "slazero2" + } + ], + "manifest": "https://raw.githubusercontent.com/slaguru666/slazero2/main/system.json", + "download": "https://github.com/slaguru666/slazero2/archive/refs/heads/main.zip", + "url": "https://github.com/slaguru666/slazero2" +} \ No newline at end of file diff --git a/template.json b/template.json new file mode 100644 index 0000000..0e7d5d6 --- /dev/null +++ b/template.json @@ -0,0 +1,332 @@ +{ + "Actor": { + "types": [ + "character", + "npc" + ], + "character": { + "biography": "", + "details": { + "age": "", + "height": "", + "weight": "", + "scl": 10, + "credits": 500, + "isPlayerCharacter": false, + "ladAccount": false, + "showingOff": 0 + }, + "race": "", + "training": [], + "archetype": "", + "attributes": { + "strength": { + "value": 2, + "label": "Strength" + }, + "agility": { + "value": 2, + "label": "Agility" + }, + "wits": { + "value": 2, + "label": "Wits" + }, + "empathy": { + "value": 2, + "label": "Empathy" + } + }, + "skills": { + "force": { + "value": 0, + "attribute": "strength", + "label": "Force" + }, + "melee": { + "value": 0, + "attribute": "strength", + "label": "Melee" + }, + "stamina": { + "value": 0, + "attribute": "strength", + "label": "Stamina" + }, + "marksmanship": { + "value": 0, + "attribute": "agility", + "label": "Marksmanship" + }, + "mobility": { + "value": 0, + "attribute": "agility", + "label": "Mobility" + }, + "stealth": { + "value": 0, + "attribute": "agility", + "label": "Stealth" + }, + "crafting": { + "value": 0, + "attribute": "wits", + "label": "Crafting" + }, + "observation": { + "value": 0, + "attribute": "wits", + "label": "Observation" + }, + "survival": { + "value": 0, + "attribute": "wits", + "label": "Survival" + }, + "healing": { + "value": 0, + "attribute": "empathy", + "label": "Healing" + }, + "insight": { + "value": 0, + "attribute": "empathy", + "label": "Insight" + }, + "persuasion": { + "value": 0, + "attribute": "empathy", + "label": "Persuasion" + }, + "ebb": { + "value": 0, + "attribute": "empathy", + "label": "Ebb" + } + }, + "derivedStats": { + "health": { + "value": 3, + "max": 3 + }, + "resolve": { + "value": 3, + "max": 3 + }, + "stress": 0, + "carryLimit": 4, + "broken": false + }, + "criticalInjuries": { + "physical": [], + "mental": [] + }, + "panic": { + "directive": "", + "active": false + }, + "flux": { + "value": 0, + "max": 0 + }, + "vevaphon": { + "instability": 0, + "instabilityMax": 12, + "activeMorphForm": "none" + }, + "specialties": "", + "equipment": "", + "weapons": "", + "finances": { + "income": { + "salary": 0, + "bpnReward": 0, + "other": 0 + }, + "expenses": { + "accommodation": 0, + "drugs": 0, + "subscriptions": 0, + "other": 0, + "bulletTax": 0 + }, + "debt": 0, + "ammoSpentSession": 0, + "creditLog": [] + }, + "bpn": { + "code": "", + "type": "blue", + "status": "open", + "reward": 0, + "description": "", + "objectives": "", + "notes": "" + } + }, + "npc": { + "biography": "", + "threat": 1, + "attributes": { + "strength": 2, + "agility": 2, + "wits": 2, + "empathy": 2 + }, + "health": { + "value": 10, + "max": 10 + }, + "armor": 0, + "damage": 2 + } + }, + "Item": { + "types": [ + "weapon", + "armor", + "equipment", + "ammo", + "specialty", + "ebb" + ], + "weapon": { + "description": "", + "weaponType": "Pistol", + "category": "ranged", + "damage": 2, + "range": "Short", + "rof": 1, + "gearBonus": 0, + "ap": 0, + "magazine": 0, + "ammo": 0, + "cost": 0, + "fireModes": [ + "single" + ], + "autoAmmoUse": 8, + "ammoType": "standard", + "caliber": "", + "roundType": "standard", + "roundCostStd": 0, + "roundCostAP": 0, + "roundCostHE": 0, + "initiativeMod": 0, + "equipped": false, + "ammoEmpty": false + }, + "armor": { + "description": "", + "armorRating": 0, + "armorDice": 0, + "armorAuto": 0, + "statMod": 0, + "skillMod": 0, + "statModTarget": "", + "skillModTarget": "", + "healthMod": 0, + "resolveMod": 0, + "cost": 0, + "initiativeMod": 0, + "equipped": false + }, + "ammo": { + "description": "", + "caliber": "10mm", + "roundType": "standard", + "quantity": 30, + "costPerRound": 5, + "apBonus": 0, + "damageMod": 0, + "blastRadius": 0, + "weight": 0 + }, + "equipment": { + "description": "", + "quantity": 1, + "cost": 0, + "weight": 1, + "isDrug": false, + "active": false, + "activeDuration": "1 shift", + "activeLabel": "", + "activeNotes": "", + "withdrawalActive": false, + "withdrawalDuration": "", + "withdrawalLabel": "", + "withdrawalNotes": "", + "healthMod": 0, + "resolveMod": 0, + "statPhysicalMod": 0, + "skillAllMod": 0, + "panicReduction": 0, + "injuryPenaltyIgnore": 0, + "stressRecoveryBonus": 0, + "postUseStaminaDamage": 0, + "wdHealthMod": 0, + "wdResolveMod": 0, + "wdStatPhysicalMod": 0, + "wdSkillAllMod": 0, + "wdPanicReduction": 0, + "wdInjuryPenaltyIgnore": 0, + "wdStressRecoveryBonus": 0, + "wdPostUseStaminaDamage": 0, + "skillMods": { + "force": 0, + "melee": 0, + "stamina": 0, + "marksmanship": 0, + "mobility": 0, + "stealth": 0, + "crafting": 0, + "observation": 0, + "survival": 0, + "healing": 0, + "insight": 0, + "persuasion": 0 + }, + "wdSkillMods": { + "force": 0, + "melee": 0, + "stamina": 0, + "marksmanship": 0, + "mobility": 0, + "stealth": 0, + "crafting": 0, + "observation": 0, + "survival": 0, + "healing": 0, + "insight": 0, + "persuasion": 0 + }, + "initiativeMod": 0, + "equipped": false + }, + "specialty": { + "description": "", + "effects": "", + "category": "general", + "prerequisites": "", + "package": "", + "healthMod": 0, + "resolveMod": 0, + "isActive": true, + "isNaturalWeapon": false, + "naturalWeaponDamage": 0, + "naturalWeaponAP": 0, + "naturalWeaponROF": 0 + }, + "ebb": { + "description": "", + "discipline": "blast", + "fluxCost": 1, + "successes": 1, + "effect": "", + "catastrophe": "", + "duration": "", + "range": "Engaged" + }, + "initiativeMod": 0 + } +} diff --git a/templates/actor/actor-crit-chatcard.hbs b/templates/actor/actor-crit-chatcard.hbs new file mode 100644 index 0000000..1c22e4c --- /dev/null +++ b/templates/actor/actor-crit-chatcard.hbs @@ -0,0 +1,10 @@ +
+
+
+ +
+

{{{result}}}

+
+
+
+
diff --git a/templates/actor/actor-damage-chatcard.hbs b/templates/actor/actor-damage-chatcard.hbs new file mode 100644 index 0000000..ffe4aa9 --- /dev/null +++ b/templates/actor/actor-damage-chatcard.hbs @@ -0,0 +1,38 @@ +
+
+ {{#if (gt damage 0)}} +

+ {{localize "FLBR.CHAT.DamageDealt" name=name amount=damage}} + {{#if broken}} +
+ + {{#if vroom}} + {{{config.Icons.buttons.pierce}}} {{toUpperCase (localize "FLBR.Wrecked")}} + {{else}} + {{{config.Icons.buttons.lethal}}} {{toUpperCase (localize "FLBR.Broken")}} + {{/if}} + + {{/if}} +

+ {{else}} +

{{localize "FLBR.CHAT.NoDamageDealt" name=name}}

+ {{/if}} + {{#if armored}} +
+

+ + {{localize "FLBR.CHAT.InitialDamageDealt" damage=initialDamage}} + {{#if (gt damage 0)}} + {{#if (eq damage initialDamage)}} + {{localize "FLBR.CHAT.ArmorNoEffect"}} + {{else}} + {{localize "FLBR.CHAT.ArmorDeflectedDamage" damage=deflectedDamage}} + {{/if}} + {{else}} + {{localize "FLBR.CHAT.ArmorDeflectedAllDamage"}} + {{/if}} + +

+ {{/if}} +
+
diff --git a/templates/actor/actor-sheet-config.hbs b/templates/actor/actor-sheet-config.hbs new file mode 100644 index 0000000..e6e8b7b --- /dev/null +++ b/templates/actor/actor-sheet-config.hbs @@ -0,0 +1,56 @@ +
+ {{#if (eq document.type "character")}} +
+ {{localize "FLBR.SHEET_CONFIG.CharacterConfiguration"}} +
+ + +

{{localize "FLBR.SHEET_CONFIG.CharacterSubtypeHint"}}

+
+ +
+ +
+ + +

{{localize "FLBR.SHEET_CONFIG.HealthMaxModifierHint"}}

+
+ {{!--
+ + +

{{localize "FLBR.SHEET_CONFIG.HealthPermanentLossHint"}}

+
--}} + + {{#if (eq document.system.subtype "pc")}} +
+ + +

{{localize "FLBR.SHEET_CONFIG.ResolveMaxModifierHint"}}

+
+
+ + +

{{localize "FLBR.SHEET_CONFIG.ResolvePermanentLossHint"}}

+
+ {{/if}} + +
+ +
+ + {{scoreSelector "system.attributes.mvr.value" selected=document.system.attributes.mvr.value}} +

{{localize "FLBR.SHEET_CONFIG.ManeuverabilityHint"}}

+
+ +
+ +
+ + +

{{localize "FLBR.SHEET_CONFIG.UnlimitedPushHint"}}

+
+
+ {{/if}} +
diff --git a/templates/actor/character-sheet-mk1.hbs b/templates/actor/character-sheet-mk1.hbs new file mode 100644 index 0000000..ac9992c --- /dev/null +++ b/templates/actor/character-sheet-mk1.hbs @@ -0,0 +1,794 @@ +
+ + {{!-- SHEET HEADER --}} +
+ {{#if drugAlerts.length}} +
+ {{#each drugAlerts}} +
+
+ {{this.name}} + {{this.duration}} +
+
{{this.summary}}
+
+ +
+
+ {{/each}} +
+ {{/if}} + + {{!-- TOP ROW: Portrait + Name + Key Stats --}} +
+
+ +
+
+
+ +
+
+
+ SCL + +
+
+ Credits + +
+
+ + +
+
+
+
+ + {{!-- CHARACTER DETAILS ROW --}} +
+
+ + +
+
+ + +
+
+ + + + +
+
+ + +
+
+ + {{!-- VITALS ROW: Health, Resolve, Stress, Ebb --}} +
+
+ +
+ + / + +
+
+
+ +
+ + / + +
+
+
+ +
+ + +
+
+ {{#if isEbbUser}} +
+ +
+ + / + {{computedFluxMax}} +
+
+ {{/if}} +
+ + {{!-- CRITICAL INJURIES ROW --}} +
+
+ + +
+ + {{!-- Active injuries display --}} + {{#if system.criticalInjuries.physical.length}} +
+

Physical Injuries

+ {{#each system.criticalInjuries.physical}} +
+
+ {{this.name}} + {{#if this.lethal}}LETHAL{{/if}} + +
+

{{this.effects}}

+
+ {{/each}} +
+ {{/if}} + + {{#if system.criticalInjuries.mental.length}} +
+

Mental Injuries

+ {{#each system.criticalInjuries.mental}} +
+
+ {{this.name}} + {{#if this.lethal}}LETHAL{{/if}} + +
+

{{this.effects}}

+
+ {{/each}} +
+ {{/if}} +
+ +
+ + {{!-- SHEET TABS --}} + + + {{!-- SHEET BODY --}} +
+ + {{!-- STATS TAB --}} +
+
Attributes & Skills
+ +
+ {{!-- STRENGTH COLUMN --}} +
+
+
Strength
+
+ +
{{system.attributes.strength.value}}
+ + +
+ +
+ +
+ +
+ + +
{{system.skills.force.value}}
+ + +
+
+ +
+ +
+ + +
{{system.skills.melee.value}}
+ + +
+
+ +
+ +
+ + +
{{system.skills.stamina.value}}
+ + +
+
+
+ + {{!-- AGILITY COLUMN --}} +
+
+
Agility
+
+ +
{{system.attributes.agility.value}}
+ + +
+ +
+ +
+ +
+ + +
{{system.skills.marksmanship.value}}
+ + +
+
+ +
+ +
+ + +
{{system.skills.mobility.value}}
+ + +
+
+ +
+ +
+ + +
{{system.skills.stealth.value}}
+ + +
+
+
+ + {{!-- WITS COLUMN --}} +
+
+
Wits
+
+ +
{{system.attributes.wits.value}}
+ + +
+ +
+ +
+ +
+ + +
{{system.skills.crafting.value}}
+ + +
+
+ +
+ +
+ + +
{{system.skills.observation.value}}
+ + +
+
+ +
+ +
+ + +
{{system.skills.survival.value}}
+ + +
+
+
+ + {{!-- EMPATHY COLUMN --}} +
+
+
Empathy
+
+ +
{{system.attributes.empathy.value}}
+ + +
+ +
+ +
+ +
+ + +
{{system.skills.healing.value}}
+ + +
+
+ +
+ +
+ + +
{{system.skills.insight.value}}
+ + +
+
+ +
+ +
+ + +
{{system.skills.persuasion.value}}
+ + +
+
+ {{#if isEbbUser}} +
+ +
+ + +
{{system.skills.ebb.value}}
+ + +
+
+ {{/if}} +
+ +
+ + {{!-- Specialties Section --}} +
+

+ Specialties & Talents +

+
+ {{#each specialties}} +
+ +
+
+ {{this.name}} + {{this.system.package}} + +
+
{{this.system.effects}}
+
+
+ {{/each}} + {{#unless specialties.length}} +
+ Drag specialties from the compendium to add them here. +
+ {{/unless}} +
+
+ +
+ + {{!-- EQUIPMENT TAB --}} +
+ + {{!-- RANGED WEAPONS --}} +
+

Ranged Weapons

+
+ {{#each rangedWeapons}} +
+ +
+
+ {{this.name}} + {{this.system.weaponType}} +
+
+ DMG {{this.system.damage}} + Range {{this.system.range}} + ROF {{this.system.rof}} + AP {{this.system.ap}} + Gear +{{this.system.gearBonus}} +
+
+
+ {{#if this.system.ammo}}{{this.system.ammo}}{{else}}0{{/if}} + / + {{#if this.system.magazine}}{{this.system.magazine}}{{else}}0{{/if}} + +
+
+ + + +
+
+ {{/each}} + {{#unless rangedWeapons.length}} +
Drag ranged weapons from the compendium.
+ {{/unless}} +
+
+ + {{!-- MELEE WEAPONS --}} +
+

Melee Weapons

+
+ {{#each meleeWeapons}} +
+ +
+
+ {{this.name}} + {{this.system.weaponType}} +
+
+ DMG {{this.system.damage}} + AP {{this.system.ap}} + Gear +{{this.system.gearBonus}} +
+
+
+ + + +
+
+ {{/each}} + {{#unless meleeWeapons.length}} +
Drag melee weapons from the compendium.
+ {{/unless}} +
+
+ + {{!-- ARMOUR --}} +
+

Armour

+
+ {{#each armors}} +
+ +
+
+ {{this.name}} + {{this.system.armorDice}}({{this.system.armorAuto}}) +
+
+ Armor {{this.system.armorDice}}({{this.system.armorAuto}}) + Stat {{#if (gt this.system.statMod 0)}}+{{/if}}{{this.system.statMod}} + Skill {{#if (gt this.system.skillMod 0)}}+{{/if}}{{this.system.skillMod}} + INIT {{#if (gt this.system.initiativeMod 0)}}+{{/if}}{{this.system.initiativeMod}} +
+
+ Stat Ref {{#if this.system.statModTarget}}{{capitalize this.system.statModTarget}}{{else}}—{{/if}} + Skill Ref {{#if this.system.skillModTarget}}{{capitalize this.system.skillModTarget}}{{else}}—{{/if}} +
+
+
+ + + +
+
+ {{/each}} + {{#unless armors.length}} +
Drag armor items here.
+ {{/unless}} +
+
+ + {{!-- GEAR ITEMS --}} +
+

Gear Items

+
+ {{#each gearItems}} +
+ +
+
+ {{this.name}} + {{#if this.system.quantity}}{{this.system.quantity}}{{else}}1{{/if}}x +
+
+ Weight {{#if this.system.weight}}{{this.system.weight}}{{else}}0{{/if}} + INIT {{#if (gt this.system.initiativeMod 0)}}+{{/if}}{{this.system.initiativeMod}} +
+
+
+ + +
+
+ {{/each}} + {{#unless gearItems.length}} +
Drag equipment items here.
+ {{/unless}} +
+
+ + {{!-- DRUGS --}} +
+

Drugs

+
+ {{#each drugs}} +
+ +
+
+ {{this.name}} + {{#if this.system.quantity}}{{this.system.quantity}}{{else}}0{{/if}}x +
+
+ {{#if this.system.active}} + ACTIVE {{#if this.system.activeDuration}}{{this.system.activeDuration}}{{else}}—{{/if}} + {{/if}} + {{#if this.system.withdrawalActive}} + WITHDRAWAL {{#if this.system.withdrawalDuration}}{{this.system.withdrawalDuration}}{{else}}—{{/if}} + {{/if}} + {{#unless this.system.active}} + {{#unless this.system.withdrawalActive}} + Status Inactive + {{/unless}} + {{/unless}} +
+
+
+ {{#if this.system.active}} + + {{else}} + {{#if this.system.withdrawalActive}} + + {{else}} + + {{/if}} + {{/if}} + +
+
+ {{/each}} + {{#unless drugs.length}} +
Drag drug items here.
+ {{/unless}} +
+
+ + {{!-- GENERAL EQUIPMENT NOTES --}} +
+

Equipment Notes

+
+ +
+
+
+ + {{!-- EBB TAB (Ebon / Brain Waster only) --}} + {{#if isEbbUser}} +
+ + {{!-- FLUX STATUS BAR --}} +
+
+
FLUX
+
+ {{!-- NO name attr here — system.flux.value is bound once in Stats tab; duplicate causes "X,X" corruption --}} + + / + {{computedFluxMax}} +
+
+ + +
+
+ + {{!-- EBB SKILL --}} +
+
Ebb Skill
+
+ + +
{{system.skills.ebb.value}}
+ + {{!-- NO hidden input here — the Stats tab already binds system.skills.ebb.value --}} +
+
Pool: Empathy {{system.attributes.empathy.value}} + Ebb {{system.skills.ebb.value}}
+
+
+ + {{!-- FORMULAE LIST --}} +
+

Known Formulae + {{ebbFormulae.length}} known +

+ + {{!-- Group formulae by discipline --}} +
+ {{#each ebbFormulae}} +
+ +
+
+ {{this.name}} + {{this.system.discipline}} +
+
+ {{this.system.fluxCost}} + SUC {{this.system.successes}} + {{#if this.system.range}}Range {{this.system.range}}{{/if}} + {{#if this.system.duration}}Dur {{this.system.duration}}{{/if}} +
+ {{#if this.system.effect}} +
+ {{this.system.effect}} +
+ {{/if}} +
+
+ + + +
+
+ {{/each}} + {{#unless ebbFormulae.length}} +
+ No formulae known. Drag Ebb items from the world item list. +
+ {{/unless}} +
+
+ +
+ {{/if}} + + {{!-- BIO TAB --}} +
+
+

Biography

+ +

Background

+
+ +
+ +

Appearance

+
+ +
+ +

Personality

+
+ +
+ +

Notes

+
+ +
+
+
+ +
+ +
diff --git a/templates/actor/character-sheet-mk2.hbs b/templates/actor/character-sheet-mk2.hbs new file mode 100644 index 0000000..629ab58 --- /dev/null +++ b/templates/actor/character-sheet-mk2.hbs @@ -0,0 +1,309 @@ +{{!-- ZERO ENGINE Mk2 — SLA Industries Dark Sheet --}} +
+ + {{!-- ═══ HEADER ══════════════════════════════════════════════════════════ --}} +
+ +
+ +
+ + + {{#if system.race.[0]}}{{system.race.[0]}}{{else}}—{{/if}} + {{#if archetype}}{{archetype}}{{else}}—{{/if}} +
+
+
+ INITIATIVE + + +
+
+ + {{!-- ═══ VITALS ═══════════════════════════════════════════════════════════ --}} +
+
+ HP + + / + +
+
+ RES + + / + +
+
+ STRESS + +
+ {{#if isEbbUser}} +
+ FLUX + + / + {{computedFluxMax}} +
+ {{/if}} +
+ + +
+
+ + {{!-- ═══ TAB NAV ══════════════════════════════════════════════════════════ --}} + + + {{!-- ═══════════════════ STATS ═══════════════════════════════════════════ --}} +
+ + {{!-- 4-column attribute+skill grid --}} +
+ {{#each attributeGroups}} +
+ {{!-- Attribute header --}} +
+ {{label}} + + +
+ {{!-- 3 skills beneath --}} + {{#each skills}} +
+ + {{label}} +
{{#each dots}}{{/each}}
+ {{value}} + +
+ {{/each}} + {{#if (eq key "empathy")}} + {{#if ../isEbbUser}} +
+ + Ebb +
{{#each ../skillsWithDots}}{{#if (eq key "ebb")}}{{#each dots}}{{/each}}{{/if}}{{/each}}
+ {{../system.skills.ebb.value}} + +
+ {{/if}} + {{/if}} +
+ {{/each}} +
+ + {{!-- Specialties --}} +
SPECIALTIES & RACIAL ABILITIES + ADD
+ {{#each items}} + {{#if (eq type "specialty")}} +
+ {{name}} + {{system.effects}} + ✎ + ✕ +
+ {{/if}} + {{/each}} + + {{!-- Armor quick-view on stats panel --}} +
ARMOUR + ADD
+ {{#each items}} + {{#if (eq type "armor")}} +
+ {{name}} + {{system.armorDice}}d6 + {{system.armorAuto}} + + ✕ +
+ {{/if}} + {{/each}} + {{#unless armors.length}}
No armour equipped
{{/unless}} + +
+ + {{!-- ═══════════════════ COMBAT ════════════════════════════════════════════ --}} +
+ +
WEAPONS + ADD
+ {{#each weaponItems}} +
+
+ {{name}} + DMG {{system.damage}} + {{#if system.ap}}AP {{system.apEffective}}{{/if}} + {{system.range}} + {{#if system.gearBonus}}+{{system.gearBonus}}{{/if}} + {{#if system.ammoEmpty}}🔒 EMPTY{{/if}} + ✎ + ✕ +
+ {{#if (eq system.category "ranged")}} +
+ {{#if caliber}}{{caliber}}{{/if}} +
+ + + +
+ +
+ {{else}} +
+ +
+ {{/if}} +
+ {{/each}} + +
ARMOR + ADD
+ {{#each items}} + {{#if (eq type "armor")}} +
+ {{name}} + {{system.armorDice}}d6 + {{system.armorAuto}} + ✎ + ✕ +
+ {{/if}} + {{/each}} + +
+ + {{!-- ═══════════════════ FINANCES ═════════════════════════════════════════ --}} +
+
{{system.details.credits}}c + {{#if system.finances.debt}}⚠ DEBT {{system.finances.debt}}c{{/if}} +
+
+
+
◈ INCOME / WEEK
+
c
+
c
+
c
+
TOTAL{{weeklyIncome}}c
+
+
+
◈ EXPENSES / WEEK
+
c
+
c
+
c
+
c
+
{{system.finances.expenses.bulletTax}}c
+
c
+
TOTAL{{weeklyExpenses}}c
+
+
+
+ WEEKLY NET + {{weeklyNetSign}}{{weeklyNet}}c +
+ {{!-- ══ BPN PANEL ══ --}} +
+
+ {{#if system.bpn.type}}{{system.bpn.type}}{{else}}BLUE{{/if}} BPN + + + +
+ + c +
+
+ + + +
+ +
AMMO RESERVES
+
+ {{#each items}} + {{#if (eq type "ammo")}} +
+ {{system.caliber}} + {{system.roundType}} + {{system.quantity}} + {{system.costPerRound}}c/rnd + {{#if system.apBonus}}AP+{{system.apBonus}}{{/if}} + {{#if system.damageMod}}DMG+{{system.damageMod}}{{/if}} +
+ {{/if}} + {{/each}} +
+
+ + {{!-- ═══════════════════ EBB ════════════════════════════════════════════════ --}} + {{#if isEbbUser}} +
+
EBB FORMULAE
+ {{#each ebbFormulae}} +
+
{{system.discipline}}
+
+ {{name}} +
+ ⚡ {{system.fluxCost}} Flux + {{system.successes}} suc + {{#if system.range}}{{system.range}}{{/if}} + {{#if system.duration}}{{system.duration}}{{/if}} +
+ {{#if system.effect}}
{{system.effect}}
{{/if}} +
+
+ + ✎ + ✕ +
+
+ {{/each}} + {{#unless ebbFormulae.length}} +
No formulae known. Drag Ebb items from the world item list.
+ {{/unless}} +
+ {{/if}} + + {{!-- ═══════════════════ GEAR ═════════════════════════════════════════════ --}} +
+
EQUIPMENT + ADD
+ {{#each items}} + {{#unless (eq type "weapon")}}{{#unless (eq type "armor")}}{{#unless (eq type "specialty")}}{{#unless (eq type "ebb")}}{{#unless (eq type "ammo")}} +
+ {{name}} + {{#if system.quantity}}×{{system.quantity}}{{/if}} + ✎ + ✕ +
+ {{/unless}}{{/unless}}{{/unless}}{{/unless}}{{/unless}} + {{/each}} +
+ + {{!-- ═══════════════════ NOTES ════════════════════════════════════════════ --}} +
+
DOSSIER
+ +
+ +
diff --git a/templates/actor/character-sheet-simple.hbs b/templates/actor/character-sheet-simple.hbs new file mode 100644 index 0000000..d2781a2 --- /dev/null +++ b/templates/actor/character-sheet-simple.hbs @@ -0,0 +1,117 @@ +
+ + {{!-- Sheet Header --}} +
+ + +
+

+ +

+ +
+
+
+ + +
+ +
+ + +
+ +
+ + +
+
+ +
+
+ + +
+
+ + +
+
+
+
+
+ + {{!-- Attributes & Skills --}} +
+

Attributes & Skills

+ +
+ {{#each system.attributes as |attribute attrKey|}} +
+

{{capitalize attrKey}}

+
+ + {{attribute.value}} + + +
+ + {{!-- Skills for this attribute --}} +
+ {{#each @root.system.skills as |skill skillKey|}} + {{#if (eq skill.attribute attrKey)}} +
+ {{skill.label}} + + {{skill.value}} + + +
+ {{/if}} + {{/each}} +
+
+ {{/each}} +
+ + {{!-- Health & Resolve --}} +
+
+ + + / + +
+ +
+ + + / + +
+ +
+ + +
+
+
+ +
diff --git a/templates/actor/character-sheet.hbs b/templates/actor/character-sheet.hbs new file mode 100644 index 0000000..ee3d04b --- /dev/null +++ b/templates/actor/character-sheet.hbs @@ -0,0 +1,1024 @@ +
+ + {{!-- SHEET HEADER --}} +
+ {{#if drugAlerts.length}} +
+ {{#each drugAlerts}} +
+
+ {{this.name}} + {{this.duration}} +
+
{{this.summary}}
+
+ +
+
+ {{/each}} +
+ {{/if}} + + {{!-- TOP ROW: Portrait + Name + Key Stats --}} +
+
+ +
+
+
+ +
+
+
+ SCL + +
+
+ Credits + +
+
+ + +
+
+
+
+ + {{!-- CHARACTER DETAILS ROW --}} +
+
+ + +
+
+ + +
+
+ + + + +
+
+ +
+ {{#each armors}}{{#if @first}}{{this.name}}{{/if}}{{/each}} + {{#unless armors.length}}None{{/unless}} +
+ +
+
+ + {{!-- VITALS ROW: Health, Resolve, Stress, Ebb --}} +
+
+ +
+ + / + +
+
+
+ +
+ + / + +
+
+
+ +
+ + / {{stressMax}} + +
+
+
+ +
+ {{#if isEbbUser}} +
+ +
+ + / + {{computedFluxMax}} +
+
+ {{/if}} + {{#if isVevaphon}} +
+ +
+ + / + 12 +
+
+ {{/if}} +
+ + {{!-- CRITICAL INJURIES ROW --}} +
+
+ + +
+ + {{!-- Active injuries display --}} + {{#if system.criticalInjuries.physical.length}} +
+

Physical Injuries

+ {{#each system.criticalInjuries.physical}} +
+
+ {{this.name}} + {{#if this.lethal}}LETHAL{{/if}} + +
+

{{this.effects}}

+
+ {{/each}} +
+ {{/if}} + + {{#if system.criticalInjuries.mental.length}} +
+

Mental Injuries

+ {{#each system.criticalInjuries.mental}} +
+
+ {{this.name}} + {{#if this.lethal}}LETHAL{{/if}} + +
+

{{this.effects}}

+
+ {{/each}} +
+ {{/if}} + + {{!-- Active conditions from SLA status effects --}} + {{#if activeConditions.length}} +
+ {{#each activeConditions}} +
+
+ + {{this.label}} + {{this.description}} +
+ +
+ {{/each}} +
+ {{/if}} +
+ +
+ + {{!-- SHEET TABS --}} + + + {{!-- SHEET BODY --}} +
+ + {{!-- STATS TAB --}} +
+
Attributes & Skills
+ +
+ {{!-- STRENGTH COLUMN --}} +
+
+
Strength
+
+ +
{{system.attributes.strength.value}}
+ + +
+ +
+ +
+ +
+ + +
{{system.skills.force.value}}
+ + +
+
+ +
+ +
+ + +
{{system.skills.melee.value}}
+ + +
+
+ +
+ +
+ + +
{{system.skills.stamina.value}}
+ + +
+
+
+ + {{!-- AGILITY COLUMN --}} +
+
+
Agility
+
+ +
{{system.attributes.agility.value}}
+ + +
+ +
+ +
+ +
+ + +
{{system.skills.marksmanship.value}}
+ + +
+
+ +
+ +
+ + +
{{system.skills.mobility.value}}
+ + +
+
+ +
+ +
+ + +
{{system.skills.stealth.value}}
+ + +
+
+
+ + {{!-- WITS COLUMN --}} +
+
+
Wits
+
+ +
{{system.attributes.wits.value}}
+ + +
+ +
+ +
+ +
+ + +
{{system.skills.crafting.value}}
+ + +
+
+ +
+ +
+ + +
{{system.skills.observation.value}}
+ + +
+
+ +
+ +
+ + +
{{system.skills.survival.value}}
+ + +
+
+
+ + {{!-- EMPATHY COLUMN --}} +
+
+
Empathy
+
+ +
{{system.attributes.empathy.value}}
+ + +
+ +
+ +
+ +
+ + +
{{system.skills.healing.value}}
+ + +
+
+ +
+ +
+ + +
{{system.skills.insight.value}}
+ + +
+
+ +
+ +
+ + +
{{system.skills.persuasion.value}}
+ + +
+
+ {{#if isEbbUser}} +
+ +
+ + +
{{system.skills.ebb.value}}
+ + +
+
+ {{/if}} +
+ +
+ + {{!-- Specialties Section --}} +
+

+ Specialties & Talents +

+
+ {{#each specialties}} +
+ +
+
+ {{this.name}} + {{this.system.package}} + {{!-- Natural weapon STRIKE button --}} + {{#if this.system.isNaturalWeapon}} + + {{/if}} + {{!-- Morph form SHIFT button for Vevaphon forms --}} + {{#if (eq this.system.prerequisites "Vevaphon")}} + {{#unless (eq this.name "Morphic Strike")}} + {{#if (eq this.name "Brute Form")}} + + {{else if (eq this.name "Stalker Form")}} + + {{else if (eq this.name "Raptor Form")}} + + {{/if}} + {{/unless}} + {{/if}} + +
+
{{this.system.effects}}
+
+
+ {{/each}} + {{#unless specialties.length}} +
+ Drag specialties from the compendium to add them here. +
+ {{/unless}} +
+
+ +
+ + {{!-- EQUIPMENT TAB --}} +
+ + {{#if isVevaphon}} +
+
Morph Form + + {{#if (gte system.vevaphon.instability 10)}}⚠ MORPH PANIC RISK{{else if (gte system.vevaphon.instability 6)}}⚠ INSTABILITY EFFECTS ACTIVE{{/if}} + +
+
+ + + + +
+
+
+ {{#each instabilityPips}}{{/each}} +
+ +
+
+ {{/if}} + + {{!-- RANGED WEAPONS --}} +
+

Ranged Weapons

+
+ {{#each rangedWeapons}} +
+ +
+
+ {{this.name}} + {{this.system.weaponType}} + {{#if this.system.ammoEmpty}} EMPTY{{/if}} +
+
+ DMG {{this.system.damage}} + Range {{this.system.range}} + ROF {{this.system.rof}} + AP {{this.system.ap}} + Gear +{{this.system.gearBonus}} +
+
+
+ +
+ + +
+
+
+ {{/each}} + {{#unless rangedWeapons.length}} +
Drag ranged weapons from the compendium.
+ {{/unless}} +
+
+ + {{!-- MELEE WEAPONS --}} +
+

Melee Weapons

+
+ {{#each meleeWeapons}} +
+ +
+
+ {{this.name}} + {{this.system.weaponType}} +
+
+ DMG {{this.system.damage}} + AP {{this.system.ap}} + Gear +{{this.system.gearBonus}} +
+
+
+ +
+ + +
+
+
+ {{/each}} + {{#unless meleeWeapons.length}} +
Drag melee weapons from the compendium.
+ {{/unless}} +
+
+ + {{!-- ARMOUR --}} +
+

Armour

+
+ {{#each armors}} +
+ +
+
+ {{this.name}} + {{this.system.armorDice}}({{this.system.armorAuto}}) +
+
+ Armor {{this.system.armorDice}}({{this.system.armorAuto}}) + Stat {{#if (gt this.system.statMod 0)}}+{{/if}}{{this.system.statMod}} + Skill {{#if (gt this.system.skillMod 0)}}+{{/if}}{{this.system.skillMod}} + INIT {{#if (gt this.system.initiativeMod 0)}}+{{/if}}{{this.system.initiativeMod}} +
+
+ Stat Ref {{#if this.system.statModTarget}}{{capitalize this.system.statModTarget}}{{else}}—{{/if}} + Skill Ref {{#if this.system.skillModTarget}}{{capitalize this.system.skillModTarget}}{{else}}—{{/if}} +
+
+
+ + + +
+
+ {{/each}} + {{#unless armors.length}} +
Drag armor items here.
+ {{/unless}} +
+
+ + {{!-- GEAR ITEMS --}} +
+

Gear Items

+
+ {{#each gearItems}} +
+ +
+
+ {{this.name}} + {{#if this.system.quantity}}{{this.system.quantity}}{{else}}1{{/if}}x +
+
+ Weight {{#if this.system.weight}}{{this.system.weight}}{{else}}0{{/if}} + INIT {{#if (gt this.system.initiativeMod 0)}}+{{/if}}{{this.system.initiativeMod}} +
+
+
+ + +
+
+ {{/each}} + {{#unless gearItems.length}} +
Drag equipment items here.
+ {{/unless}} +
+
+ + {{!-- DRUGS --}} +
+

Drugs

+
+ {{#if drugWithdrawal}} +
+ + DRUG WITHDRAWAL — −1 die to all rolls until a drug is activated +
+ {{/if}} + {{#each drugs}} +
+ +
+
+ {{this.name}} + {{#if this.system.quantity}}{{this.system.quantity}}{{else}}0{{/if}}x +
+
+ {{#if this.system.active}} + ACTIVE {{#if this.system.activeDuration}}{{this.system.activeDuration}}{{else}}—{{/if}} + {{/if}} + {{#if this.system.withdrawalActive}} + WITHDRAWAL {{#if this.system.withdrawalDuration}}{{this.system.withdrawalDuration}}{{else}}—{{/if}} + {{/if}} + {{#unless this.system.active}} + {{#unless this.system.withdrawalActive}} + Status Inactive + {{/unless}} + {{/unless}} +
+
+
+ {{#if this.system.active}} + + {{else}} + {{#if this.system.withdrawalActive}} + + {{else}} + + {{/if}} + {{/if}} + +
+
+ {{/each}} + {{#unless drugs.length}} +
Drag drug items here.
+ {{/unless}} +
+
+ + {{!-- GENERAL EQUIPMENT NOTES --}} +
+

Equipment Notes

+
+ +
+
+
+ + {{!-- EBB TAB (Ebon / Brain Waster only) --}} + {{#if isEbbUser}} +
+ + {{!-- FLUX STATUS BAR --}} +
+
+
FLUX
+
+ {{!-- NO name attr here — system.flux.value is bound once in Stats tab; duplicate causes "X,X" corruption --}} + + / + {{computedFluxMax}} +
+
+ + +
+
+ + {{!-- EBB SKILL --}} +
+
Ebb Skill
+
+ + +
{{system.skills.ebb.value}}
+ + {{!-- NO hidden input here — the Stats tab already binds system.skills.ebb.value --}} +
+
Pool: Empathy {{system.attributes.empathy.value}} + Ebb {{system.skills.ebb.value}}
+
+
+ + {{!-- FORMULAE LIST --}} +
+

Known Formulae + {{ebbFormulae.length}} known +

+ + {{!-- Group formulae by discipline --}} +
+ {{#each ebbFormulae}} +
+ +
+
+ {{this.name}} + {{this.system.discipline}} +
+
+ {{this.system.fluxCost}} + SUC {{this.system.successes}} + {{#if this.system.range}}Range {{this.system.range}}{{/if}} + {{#if this.system.duration}}Dur {{this.system.duration}}{{/if}} +
+ {{#if this.system.effect}} +
+ {{this.system.effect}} +
+ {{/if}} +
+
+ + + +
+
+ {{/each}} + {{#unless ebbFormulae.length}} +
+ No formulae known. Drag Ebb items from the world item list. +
+ {{/unless}} +
+
+ +
+ {{/if}} + + {{!-- BIO TAB --}} + {{!-- ══ FINANCES TAB ══ --}} +
+
+ +
+ CURRENT BALANCE + c + {{#if system.finances.debt}}⚠ DEBT {{system.finances.debt}}c{{/if}} +
+
+ ✨ SHOWING OFF +
+ + pts +
+ {{#if (gte system.details.showingOff 20)}}⚠ 20 PT MILESTONE — GM TO ACT{{/if}} +
+ +
+
+
Weekly Income
+
c
+
c
+
c
+
{{weeklyIncome}}c
+
+
+
Weekly Expenses
+
c
+
c
+
c
+ {{#if system.details.ladAccount}} +
+ +
c
+
+ {{/if}} +
c
+
{{system.finances.expenses.bulletTax}}c
+
c
+
{{weeklyExpenses}}c
+
+
+ +
+ Net Weekly Income + {{weeklyNetSign}}{{weeklyNet}}c +
+ + {{!-- ══ BPN PANEL ══ --}} +
+
+
+ {{#if system.bpn.type}}{{system.bpn.type}}{{else}}BLUE{{/if}} BPN + +
+
+ + +
+ Reward +
+ + c +
+
+
+
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+
+ +
+
+

Biography

+ +
+ + When ticked, this actor appears in the GM Finance Ledger. +
+
+ + Life After Death media subscription — costs 50c per week (auto-added to Finances). +
+ +

Background

+
+ +
+ +

Appearance

+
+ +
+ +

Personality

+
+ +
+ +

Notes

+
+ +
+
+
+ +
+ +
diff --git a/templates/actor/character/character-limited-sheet.hbs b/templates/actor/character/character-limited-sheet.hbs new file mode 100644 index 0000000..250c56e --- /dev/null +++ b/templates/actor/character/character-limited-sheet.hbs @@ -0,0 +1,56 @@ +
+ + +
+
+ +
+ +
+ +
+ +
+ +
+

{{localize "FLBR.Nature"}}

+ +
+ +
+ {{#if system.health}} +
+

{{localize "FLBR.HEADER.Health"}}?/?

+
+ {{#times 4}} + {{{@root.config.Icons.boxes.empty}}} + {{/times}} +
+
+ {{/if}} + {{#if system.resolve}} +
+

{{localize "FLBR.HEADER.Resolve"}}?/?

+
+ {{#times 4}} + {{{@root.config.Icons.boxes.empty}}} + {{/times}} +
+
+ {{/if}} +
+ +
+ +
+ +
+
+ +
+ {{{editor system.description target="system.description" button=true editable=editable}}} +
+ +
diff --git a/templates/actor/character/character-sheet.hbs b/templates/actor/character/character-sheet.hbs new file mode 100644 index 0000000..44af0fb --- /dev/null +++ b/templates/actor/character/character-sheet.hbs @@ -0,0 +1,137 @@ +
+ + {{!-- SHEET HEADER --}} + +
+
+ +
+ +
+ +
+ +
+ +
+

{{localize "FLBR.Nature"}}

+ +
+ +
+ {{#if system.health}} +
+

+ {{localize "FLBR.HEADER.Health"}} + {{#if (lte system.health.value 0)}} + ({{toUpperCase (localize "FLBR.BrokenByDamage")}}) + {{/if}} + {{system.health.value}}/{{system.health.max}} +

+ {{boxes "system.health.value" value=system.health.value min=0 max=system.health.max loss=system.health.permanentLoss}} +
+ {{/if}} + {{#if (and system.resolve isPC)}} +
+

+ {{localize "FLBR.HEADER.Resolve"}} + {{#if (lte system.resolve.value 0)}} + ({{toUpperCase (localize "FLBR.BrokenByStress")}}) + {{/if}} + {{system.resolve.value}}/{{system.resolve.max}} +

+ {{boxes "system.resolve.value" value=system.resolve.value min=0 max=system.resolve.max loss=system.resolve.permanentLoss}} +
+ {{/if}} +
+ +
+
+

Race/Species

+ +
+ +
+

Training Package

+ +
+ +
+

Archetype

+ + + {{selectOptions config.archetypes localize=true}} + +
+
+ +
+
+ + {{!-- SHEET TABS --}} + + + + {{!-- SHEET BODY --}} + +
+
+{{> systems/slazero2/templates/actor/character/sheet-tabs/stats-tab.hbs}} +
+
+{{> systems/blade-runner/templates/actor/character/sheet-tabs/mods-tab.hbs}} +
+
+{{> systems/blade-runner/templates/actor/character/sheet-tabs/combat-tab.hbs}} +
+
+{{> systems/blade-runner/templates/actor/character/sheet-tabs/inventory-tab.hbs}} +
+
+ {{#if isPC}} +{{> systems/slazero2/templates/actor/character/sheet-tabs/bio-tab.hbs}} + {{else if isNPC}} +
+ {{{editor system.description target="system.description" button=true editable=editable}}} +
+ {{/if}} +
+ +
+ + {{!-- SHEET FOOTER --}} + + {{!-- --}} + +
diff --git a/templates/actor/character/sheet-tabs/bio-tab.hbs b/templates/actor/character/sheet-tabs/bio-tab.hbs new file mode 100644 index 0000000..d2974c0 --- /dev/null +++ b/templates/actor/character/sheet-tabs/bio-tab.hbs @@ -0,0 +1,38 @@ +
+
+

{{localize "FLBR.HEADER.Notes"}}

+
+ {{{editor system.description target="system.description" button=true editable=editable}}} +
+
+ {{#if system.bio}} +
+

{{localize "FLBR.HEADER.YearsOnTheForce"}}

+ +
+
+

{{localize "FLBR.HEADER.Appearance"}}

+
+ {{{editor system.bio.appearance target="system.bio.appearance" button=true editable=editable}}} +
+
+
+

{{localize "FLBR.HEADER.Home"}}

+
+ {{{editor system.bio.home target="system.bio.home" button=true editable=editable}}} +
+
+
+

{{localize "FLBR.HEADER.KeyMemory"}}

+
+ {{{editor system.bio.keyMemory target="system.bio.keyMemory" button=true editable=editable}}} +
+
+
+

{{localize "FLBR.HEADER.KeyRelationship"}}

+
+ {{{editor system.bio.keyRelationship target="system.bio.keyRelationship" button=true editable=editable}}} +
+
+ {{/if}} +
diff --git a/templates/actor/character/sheet-tabs/combat-tab.hbs b/templates/actor/character/sheet-tabs/combat-tab.hbs new file mode 100644 index 0000000..1ab7fc6 --- /dev/null +++ b/templates/actor/character/sheet-tabs/combat-tab.hbs @@ -0,0 +1,21 @@ +
+
+

{{localize "FLBR.HEADER.Weapons"}}{{createNewItemButton "weapon"}}

+{{> systems/blade-runner/templates/actor/inventory-partial.hbs items=actor.itemTypes.weapon}} +{{> systems/blade-runner/templates/actor/inventory-partial.hbs items=actor.itemTypes.explosive}} +
+
+

{{localize "FLBR.HEADER.Armor"}}{{createNewItemButton "armor"}}

+{{> systems/blade-runner/templates/actor/inventory-partial.hbs items=actor.itemTypes.armor}} +
+
+

{{localize "FLBR.HEADER.Injuries"}}{{createNewItemButton "injury"}}

+{{> systems/blade-runner/templates/actor/inventory-partial.hbs items=actor.itemTypes.injury}} +
+
+

{{localize "FLBR.HEADER.Actions"}}

+ {{#each actions}} + {{{@root.config.Icons.buttons.action}}}{{localize label}} + {{/each}} +
+
diff --git a/templates/actor/character/sheet-tabs/inventory-tab.hbs b/templates/actor/character/sheet-tabs/inventory-tab.hbs new file mode 100644 index 0000000..e46cad0 --- /dev/null +++ b/templates/actor/character/sheet-tabs/inventory-tab.hbs @@ -0,0 +1,47 @@ +
+ {{#if isPC}} +
+ {{!--
+

{{localize "FLBR.HEADER.FavoredGear"}}

+ +
--}} +
+

{{localize "FLBR.HEADER.SignatureItem"}}

+
+ + + +
+
+
+ {{/if}} + +
+

{{localize "FLBR.HEADER.Inventory"}}{{createNewItemButton "generic"}}

+ {{> systems/blade-runner/templates/actor/inventory-partial.hbs items=actor.itemTypes.generic wrap=true}} +
+ + {{#each config.customItemTypesTemplates as | template |}} +
+

{{localize (concat "TYPES.Item." @key)}}{{createNewItemButton @key}}

+ {{> (template) items=(lookup ../actor.itemTypes @key)}} +
+ {{/each}} + + {{#if isPC}} +
+
+

{{localize "FLBR.HEADER.PromotionPoints"}}{{system.metaCurrencies.promotion}}

+ {{boxes "system.metaCurrencies.promotion" value=system.metaCurrencies.promotion min=0 max=config.maxPromotionPoints}} +
+
+

{{localize "FLBR.HEADER.HumanityPoints"}}{{system.metaCurrencies.humanity}}

+ {{boxes "system.metaCurrencies.humanity" value=system.metaCurrencies.humanity min=0 max=config.maxHumanityPoints}} +
+
+

{{localize "FLBR.HEADER.ChinyenPoints"}}{{system.metaCurrencies.chinyen}}

+ {{boxes "system.metaCurrencies.chinyen" value=system.metaCurrencies.chinyen min=0 max=config.maxChinyenPoints}} +
+
+ {{/if}} +
diff --git a/templates/actor/character/sheet-tabs/mods-tab.hbs b/templates/actor/character/sheet-tabs/mods-tab.hbs new file mode 100644 index 0000000..bf983fc --- /dev/null +++ b/templates/actor/character/sheet-tabs/mods-tab.hbs @@ -0,0 +1,21 @@ +
+
+

{{localize "FLBR.HEADER.Specialties"}}{{createNewItemButton "specialty"}}

+ {{> systems/blade-runner/templates/actor/inventory-partial.hbs items=actor.itemTypes.specialty}} +
+
+

{{localize "FLBR.HEADER.Augments"}}{{createNewItemButton "upgrade"}}

+ {{> systems/blade-runner/templates/actor/inventory-partial.hbs items=actor.itemTypes.upgrade}} +
+ {{#if showEffects}} +
+

+ {{localize "DOCUMENT.ActiveEffects"}} + + {{{@root.config.Icons.buttons.plus}}} + +

+{{> systems/blade-runner/templates/item/active-effects-partial.hbs}} +
+ {{/if}} +
diff --git a/templates/actor/character/sheet-tabs/stats-tab.hbs b/templates/actor/character/sheet-tabs/stats-tab.hbs new file mode 100644 index 0000000..27aa5a1 --- /dev/null +++ b/templates/actor/character/sheet-tabs/stats-tab.hbs @@ -0,0 +1,75 @@ +
+ {{#each system.attributes as | attribute attrKey |}} + {{#unless (eq attrKey "mvr")}} +
+
+ +
+ {{scoreSelector (concat "system.attributes." attrKey ".value") selected=attribute.value}} +
+
+ {{#if (lte attribute.value 0)}}—{{else}}{{localize "FLBR.D"}}{{attribute.value}}{{/if}} +
+
+
+ {{#each @root.system.skills as | skill skillKey |}} + {{#if (eq attrKey (lookup @root.config.skillMap skillKey))}} +
+ + {{localize (concat "FLBR.SKILL." (capitalize skillKey))}} + +
+ {{scoreSelector (concat "system.skills." skillKey ".value") selected=skill.value}} +
+
+ {{#if (lte skill.value 0)}}—{{else}}{{localize "FLBR.D"}}{{skill.value}}{{/if}} +
+
+ {{/if}} + {{/each}} +
+
+ {{/unless}} + {{/each}} +
+ +{{!-- Specialties Section --}} +
+

+ Specialties + ({{actor.items.size}}) +

+
+
+ {{#if (gt actor.items.size 0)}} +
    + {{#each actor.items as |item|}} + {{#if (eq item.type "specialty")}} +
  • +
    + {{item.name}} +

    {{item.name}}

    +
    + + +
    +
    +
    + {{item.system.description}} +
    +
  • + {{/if}} + {{/each}} +
+ {{else}} +
+ +

No specialties yet. Drag and drop specialty items here.

+
+ {{/if}} +
+
+
diff --git a/templates/actor/inventory-partial.hbs b/templates/actor/inventory-partial.hbs new file mode 100644 index 0000000..609af0a --- /dev/null +++ b/templates/actor/inventory-partial.hbs @@ -0,0 +1,125 @@ +{{!-- The parameters are: --}} +{{!-- {Item[]} items - See actor.itemTypes. --}} +{{!-- {boolean} wrap - Whether to wrap items in rows or not. --}} +
+ {{#each items}} +
+
+ +
+ +
+
+ {{#if system.lethal}} + {{{@root.config.Icons.buttons.lethal}}} + {{/if}} + {{detailedName}} +
+ + {{#if system.special}} +
{{system.special}}
+ {{/if}} + + {{!-- Weapons & Explosives --}} + {{#if isOffensive}} + {{#if (eq type "explosive")}} +
+ {{localize "FLBR.Blast"}} {{localize "FLBR.D"}}{{system.blast}} +
+ {{else if hasAttack}} +
+ {{#each system.attacks}} +
+ {{#if (eq damageType 1)}} + {{{@root.config.Icons.buttons.crush}}}{{damage}} + {{else if (eq damageType 2)}} + {{{@root.config.Icons.buttons.pierce}}}{{damage}} + {{else if (eq damageType 3)}} + {{{@root.config.Icons.buttons.stress}}}{{damage}} + {{else}} + {{damage}} + {{/if}} +
+
+ {{#unless (eq crit 0)}} + {{{@root.config.Icons.buttons.crit}}}{{localize "FLBR.D"}}{{crit}} + {{/unless}} +
+
+ {{#if (gt range.max 0)}} + {{{@root.config.Icons.buttons.range}}}{{localize (lookup @root.config.ranges range.max)}} + {{/if}} +
+ {{/each}} +
+ {{/if}} + {{!-- Armors --}} + {{else if system.armor includeZero=true}} +
+ {{localize "TYPES.Item.armor"}} {{localize "FLBR.D"}}{{system.armor}} +
+ {{/if}} + + {{#if system.description}} + {{#unless isPhysical}} +
+ {{{enrichDocumentName system.description}}} +
+ {{/unless}} + {{else if hasModifier}} +
{{modifiersDescription}}
+ {{/if}} +
+ + {{!-- Item Controls --}} + +
+ {{#if system.blast}} + + {{{@root.config.Icons.buttons.bomb}}} + + {{/if}} + {{#if (eq type "armor")}} + + {{{@root.config.Icons.buttons.armor}}} + + {{/if}} + {{#if hasAction}} + + {{{@root.config.Icons.buttons.roll}}} + + {{/if}} + + {{{@root.config.Icons.buttons.edit}}} + + + {{{@root.config.Icons.buttons.chat}}} + + {{#if (and hasAttack (eq @root.actor.type "vehicle"))}} + {{#if system.mounted}} + + {{{@root.config.Icons.buttons.unmount}}} + + {{else}} + + {{{@root.config.Icons.buttons.mount}}} + + {{/if}} + {{/if}} + {{#unless system.mounted}} + + {{{@root.config.Icons.buttons.remove}}} + + + {{/unless}} +
+ +
+ {{else}} +
+ {{localize "FLBR.InventoryNoItem"}} +
+ {{/each}} +
diff --git a/templates/actor/loot/loot-sheet.hbs b/templates/actor/loot/loot-sheet.hbs new file mode 100644 index 0000000..23c7359 --- /dev/null +++ b/templates/actor/loot/loot-sheet.hbs @@ -0,0 +1,27 @@ +
+
+ +
+ +
+ +
+ +
+ +
+

{{localize "FLBR.HEADER.Notes"}}

+
+ {{{editor system.description target="system.description" button=true editable=editable}}} +
+
+ +
{{localize "TYPES.Actor.loot"}}
+ +
+

{{localize "FLBR.HEADER.Inventory"}}

+{{> systems/blade-runner/templates/actor/inventory-partial.hbs items=items}} +
+ +
+
diff --git a/templates/actor/npc-sheet.hbs b/templates/actor/npc-sheet.hbs new file mode 100644 index 0000000..5fcf320 --- /dev/null +++ b/templates/actor/npc-sheet.hbs @@ -0,0 +1,205 @@ +
+ + {{!-- SHEET HEADER --}} +
+ + {{!-- TOP ROW: Portrait + Name --}} +
+
+ +
+
+
+ +
+
+
+ Threat + +
+
+ Armor + +
+
+
+
+ + {{!-- VITALS ROW: Health --}} +
+
+ +
+ + / + +
+
+
+ + +
+
+ + {{!-- Active conditions from SLA status effects --}} + {{#if activeConditions.length}} +
+ {{#each activeConditions}} +
+ + {{this.label}} + +
+ {{/each}} +
+ {{/if}} + +
+ + {{!-- SHEET TABS --}} + + + {{!-- SHEET BODY --}} +
+ + {{!-- STATS TAB --}} +
+
Attributes
+
+ +
+
+
Strength
+
+
{{system.attributes.strength}}
+ +
+
+
+ +
+
+
Agility
+
+
{{system.attributes.agility}}
+ +
+
+
+ +
+
+
Wits
+
+
{{system.attributes.wits}}
+ +
+
+
+ +
+
+
Empathy
+
+
{{system.attributes.empathy}}
+ +
+
+
+ +
+
+ + {{!-- COMBAT TAB --}} +
+ + {{!-- RANGED WEAPONS --}} +
+

Ranged Weapons

+
+ {{#each rangedWeapons}} +
+ +
+
+ {{this.name}} + {{this.system.weaponType}} +
+
+ DMG {{this.system.damage}} + Range {{this.system.range}} + AP {{this.system.ap}} +
+
+
+ +
+
+ {{/each}} + {{#unless rangedWeapons.length}} +
No ranged weapons.
+ {{/unless}} +
+
+ + {{!-- MELEE WEAPONS --}} +
+

Melee Weapons

+
+ {{#each meleeWeapons}} +
+ +
+
+ {{this.name}} + {{this.system.weaponType}} +
+
+ DMG {{this.system.damage}} + AP {{this.system.ap}} +
+
+
+ +
+
+ {{/each}} + {{#unless meleeWeapons.length}} +
No melee weapons. Base damage: {{system.damage}}.
+ {{/unless}} +
+
+ +
+ + {{!-- BIO TAB --}} +
+
+

Biography / Notes

+
+ +
+
+
+ +
+ +
diff --git a/templates/actor/vehicle/vehicle-seat-partial.hbs b/templates/actor/vehicle/vehicle-seat-partial.hbs new file mode 100644 index 0000000..a11b028 --- /dev/null +++ b/templates/actor/vehicle/vehicle-seat-partial.hbs @@ -0,0 +1,42 @@ +{{#with occupant}} +
+ + {{#if (lt system.health.ratio 1)}} +
+
+
+ {{/if}} + {{#if (and (eq system.subtype "pc") (lt system.resolve.ratio 1))}} +
+
+
+ {{/if}} + {{#if isBroken}} +
{{{@root.config.Icons.buttons.lethal}}}
+ {{/if}} +
+{{else}} + {{#if (and @root.isGM (eq seat -1))}} +
+
{{{@root.config.Icons.buttons.plus}}}
+
+ {{else if (eq seat -1)}} + {{! Nothing}} + {{else}} +
+
+ {{#if (eq seat 1)}} + {{{@root.config.Icons.buttons.wheel}}} + {{else}} + {{{@root.config.Icons.buttons.seat}}} + {{/if}} +
+
+ {{/if}} +{{/with}} diff --git a/templates/actor/vehicle/vehicle-sheet.hbs b/templates/actor/vehicle/vehicle-sheet.hbs new file mode 100644 index 0000000..c8255e8 --- /dev/null +++ b/templates/actor/vehicle/vehicle-sheet.hbs @@ -0,0 +1,176 @@ +
+ + {{!-- SHEET HEADER --}} + +
+
+ +
+ +
+ +
+ +
+ +
+
+ + {{#with system.maneuverability}} +
+ {{scoreSelector "system.maneuverability" selected=this}} +
+
+
+ {{#if (lte this 0)}} + {{localize "FLBR.NO_VALUE"}} + {{else}} + {{localize "FLBR.D"}}{{this}} + {{/if}} +
+
+ {{/with}} +
+
+
+

+ {{localize "FLBR.HEADER.Hull"}} + {{#if (lte system.hull.value 0)}} + ({{toUpperCase (localize "FLBR.Wrecked")}}) + {{/if}} + {{system.hull.value}}/{{system.hull.max}} +

+ {{boxes "system.hull.value" value=system.hull.value min=0 max=system.hull.max}} +
+
+
+
+ + {{scoreSelector "system.armor" selected=system.armor}} +
+
+
+ + +
+
+
+ + +
+
+
+ +
{{localize "TYPES.Actor.vehicle"}}
+ +
+

{{trim (localize "FLBR.VEHICLE.Crew") 7}}

+ {{#times system.passengers}} +{{> systems/blade-runner/templates/actor/vehicle/vehicle-seat-partial.hbs occupant=(lookup ../crew (add @index -1)) seat=@index}} + {{/times}} +{{> systems/blade-runner/templates/actor/vehicle/vehicle-seat-partial.hbs seat=-1}} +
+ +
+
+ + {{!-- SHEET TABS --}} + + + + {{!-- SHEET BODY --}} + +
+ +
+
+ + {{!-- Mounted Weapons --}} +
+

{{localize "FLBR.VEHICLE.MountedWeapons"}}

+{{> systems/blade-runner/templates/actor/inventory-partial.hbs items=mountedWeapons wrap=true}} +
+ +
+ + {{!-- Altitude --}} +
+ + +
+ + {{!-- Components --}} +
+

{{localize "FLBR.VEHICLE.Components"}}

+
+ {{#each system.components}} + + {{/each}} +
+
+ + {{!-- Actions --}} +
+

{{localize "FLBR.HEADER.Actions"}}

+ {{#each actions}} + {{{@root.config.Icons.buttons.action}}}{{localize label}} + {{/each}} +
+ +
+ + {{!-- Active Effects --}} + {{#if showEffects}} +
+

+ {{localize "DOCUMENT.ActiveEffects"}} + + {{{@root.config.Icons.buttons.plus}}} + +

+ {{> systems/blade-runner/templates/item/active-effects-partial.hbs}} +
+ {{/if}} + +
+
+ +
+
+{{> systems/blade-runner/templates/actor/inventory-partial.hbs items=trunk wrap=true}} +
+
+ +
+
+ {{{editor system.description target="system.description" button=true editable=editable}}} +
+
+ +
+ +
diff --git a/templates/components/dialog/range-picker-dialog.hbs b/templates/components/dialog/range-picker-dialog.hbs new file mode 100644 index 0000000..94f875b --- /dev/null +++ b/templates/components/dialog/range-picker-dialog.hbs @@ -0,0 +1,9 @@ +
+ {{#if description}} +

{{{description}}}

+ {{/if}} +
+ +
{{value}}
+
+
\ No newline at end of file diff --git a/templates/components/dialog/table-draw-dialog.hbs b/templates/components/dialog/table-draw-dialog.hbs new file mode 100644 index 0000000..de20573 --- /dev/null +++ b/templates/components/dialog/table-draw-dialog.hbs @@ -0,0 +1,31 @@ +
+
+ +
+
+ + +
+ {{#unless hideFormula}} +
+ + +
+ {{/unless}} +
diff --git a/templates/components/roll/roll-askdie-dialog.hbs b/templates/components/roll/roll-askdie-dialog.hbs new file mode 100644 index 0000000..cc3f3c2 --- /dev/null +++ b/templates/components/roll/roll-askdie-dialog.hbs @@ -0,0 +1,5 @@ +
+
+ {{scoreSelector "diesize" selected=lowest}} +
+
\ No newline at end of file diff --git a/templates/components/roll/roll-chatcard-infos-partial.hbs b/templates/components/roll/roll-chatcard-infos-partial.hbs new file mode 100644 index 0000000..8e542ba --- /dev/null +++ b/templates/components/roll/roll-chatcard-infos-partial.hbs @@ -0,0 +1,24 @@ +{{#if roll.successCount}} +{{#if roll.options.damage includeZero=true}} +
+ {{localize "YZUR.CHAT.ROLL.Infos"}} +
+ {{#if roll.options.damage includeZero=true}} +
{{localize "YZUR.CHAT.ROLL.Damage"}}
+
+ {{roll.options.damage}}  + ({{localize roll.options.damageTypeName}}) +
+ {{/if}} + {{#if (and roll.options.crit (gte roll.successCount 2))}} +
{{localize "FLBR.CRIT.Crit"}}
+ + {{/if}} +
+
+{{/if}} +{{/if}} diff --git a/templates/components/roll/roll-chatcard-tooltip-partial.hbs b/templates/components/roll/roll-chatcard-tooltip-partial.hbs new file mode 100644 index 0000000..17816cd --- /dev/null +++ b/templates/components/roll/roll-chatcard-tooltip-partial.hbs @@ -0,0 +1,36 @@ +
+
+
+ {{#each parts}} +
+
+
+ {{this.formula}} + {{#if this.flavor}}{{this.flavor}}{{/if}} + {{this.total}} +
+
    + {{#if ../pushed}} + {{#each this.rolls as | lineRolls |}} +
    + {{#each lineRolls}} + {{#if this}} + {{{this.result}}} + {{else}} +   + {{/if}} + {{/each}} +
    + {{/each}} + {{else}} + {{#each this.rolls}} +
  1. {{{this.result}}}
  2. + {{/each}} + {{/if}} +
+
+
+ {{/each}} +
+
+
diff --git a/templates/components/roll/roll-chatcard.hbs b/templates/components/roll/roll-chatcard.hbs new file mode 100644 index 0000000..76d02e5 --- /dev/null +++ b/templates/components/roll/roll-chatcard.hbs @@ -0,0 +1,64 @@ +
+
+
+ {{#if flavor}} + {{flavor}} + {{/if}} +
+
{{formula}}
+ {{{tooltip}}} + {{#if isPrivate}} +

{{total}}

+ {{else}} +

+ {{#with roll}} + {{#if successCount}} + + {{#times successCount}} + + {{/times}} + + {{/if}} + {{#if baneCount}} + + {{#times baneCount}}F{{/times}} + + {{/if}} + {{#unless (or successCount baneCount)}} + 0 + {{/unless}} + {{/with}} +

+ {{/if}} +
+
+ + {{!-- Extra info added for Year Zero rolls --}} + {{#if showInfos}} + {{{infos}}} + {{/if}} + + {{!-- Push button --}} + {{#if pushable}} +
+ + +
+ {{/if}} +
+
diff --git a/templates/components/roll/roll-push-select-dialog.hbs b/templates/components/roll/roll-push-select-dialog.hbs new file mode 100644 index 0000000..985f6b3 --- /dev/null +++ b/templates/components/roll/roll-push-select-dialog.hbs @@ -0,0 +1,21 @@ +
+ {{#each dice as | die |}} + {{#each die.results}} + {{#if active}} +
+ +
+ {{/if}} + {{/each}} + {{/each}} +
diff --git a/templates/components/roll/roller.hbs b/templates/components/roll/roller.hbs new file mode 100644 index 0000000..1660240 --- /dev/null +++ b/templates/components/roll/roller.hbs @@ -0,0 +1,77 @@ +
+ + {{!--
+

{{title}}

+
--}} + +
+ {{!-- Dice --}} +
+ {{#each dice}} +
{{localize "FLBR.D"}}{{.}}
+ {{/each}} + {{#if (lt dice.length config.maxRolledDice)}} + + {{/if}} +
+ {{!-- Modifiers --}} +
+

{{localize "FLBR.HEADER.Modifiers"}}

+

{{localize "FLBR.ROLLER.ModifierActive"}}

+

{{localize "FLBR.ROLLER.ModifierName"}}

+

{{localize "FLBR.ROLLER.ModifierValue"}}

+ {{#each modifiers}} + + + {{!--
{{signedValue}}
--}} +
+ {{#if (gt value 0)}} +
+ {{{@root.config.Icons.buttons.advantage}}} +
+ {{else if (lt value 0)}} +
+ {{{@root.config.Icons.buttons.disadvantage}}} +
+ {{else}} +
{{localize "FLBR.NO_VALUE"}}
+ {{/if}} +
+ {{/each}} +
+
+ {{!-- Max Pushes --}} + {{#unless options.disabledPush}} +

{{localize "FLBR.ROLLER.MaxPush"}}

+
+ + + +
+ {{/unless}} + {{!-- RollMode --}} +

{{localize "CHAT.RollDefault"}}

+ + {{#if damage includeZero=true}} +

{{localize "FLBR.WeaponDamage"}}:{{damage}}, {{localize (lookup config.damageTypes damageType)}}

+ {{/if}} +
+
+ + + +
diff --git a/templates/item/actions-partial.hbs b/templates/item/actions-partial.hbs new file mode 100644 index 0000000..e3d51f0 --- /dev/null +++ b/templates/item/actions-partial.hbs @@ -0,0 +1,47 @@ +{{#each system.actions as | action |}} +
+ +
+ + +
+ {{#if (eq action.type "skill")}} +
+ + +
+
+ + +
+ {{else if (eq action.type "macro")}} +
+ +
+ {{/if}} +
+{{/each}} diff --git a/templates/item/active-effects-partial.hbs b/templates/item/active-effects-partial.hbs new file mode 100644 index 0000000..9faa47a --- /dev/null +++ b/templates/item/active-effects-partial.hbs @@ -0,0 +1,19 @@ +
+ {{#each effects}} + + {{else}} +
{{localize "FLBR.ACTIVE_EFFECT.Empty"}}
+ {{/each}} + {{#if inActor}} +
{{localize "FLBR.ACTIVE_EFFECT.InActorDisclaimer"}}
+ {{/if}} +
diff --git a/templates/item/attacks-partial.hbs b/templates/item/attacks-partial.hbs new file mode 100644 index 0000000..eac5ac0 --- /dev/null +++ b/templates/item/attacks-partial.hbs @@ -0,0 +1,45 @@ +
+ {{#each system.attacks as | atk |}} +
+ {{atk.name}}  + + {{{@root.config.Icons.buttons.delete}}} + + + +
+ + +
+
+ + +
+
+ + {{scoreSelector (concat "system.attacks." @key ".crit") selected=atk.crit}} +
+
+ + +
+
+ +
+ + +
+
+
+
+
+ {{else}} +
{{localize "FLBR.ItemNoAttack"}}
+ {{/each}} +
diff --git a/templates/item/item-chatcard.hbs b/templates/item/item-chatcard.hbs new file mode 100644 index 0000000..9c5bac3 --- /dev/null +++ b/templates/item/item-chatcard.hbs @@ -0,0 +1,66 @@ +
+

{{name}}

+ {{#if system.description}} +
{{{system.description}}}
+ {{/if}} + {{#if showProperties}} + {{#if (or (eq type "explosive") (eq type "armor") hasModifier system.consumable system.special)}} +
+ {{#if (eq type "explosive")}} +
{{localize "FLBR.BlastPower"}}
+
2x{{localize "FLBR.D"}}{{system.blast}}
+ {{else if (eq type "armor")}} +
{{localize "FLBR.ItemArmor"}}
+
{{localize "FLBR.D"}}{{system.armor}}
+ {{/if}} + {{#if hasModifier}} +
{{localize "FLBR.ItemModifier"}}
+
{{modifiersDescription}}
+ {{/if}} + {{#if system.consumable}} +
{{localize "FLBR.ItemConsumable"}}
+ {{/if}} + {{#if system.special}} +
{{system.special}}
+ {{/if}} +
+ {{/if}} + {{#if hasAttack}} + {{#each system.attacks as | atk |}} +
+ {{atk.name}} +
+
{{localize "FLBR.WeaponDamage"}}
+
+ {{atk.damage}}  + ({{localize (lookup @root.config.damageTypes atk.damageType)}}) +
+
{{localize "FLBR.WeaponCrit"}}
+
{{localize "FLBR.D"}}{{atk.crit}}
+
{{localize "FLBR.WeaponRange"}}
+
+ {{#if (eq atk.range.min atk.range.max)}} + {{localize (lookup @root.config.ranges atk.range.min)}} + {{else}} + {{localize (lookup @root.config.ranges atk.range.min)}} – + {{localize (lookup @root.config.ranges atk.range.max)}} + {{/if}} +
+
+
+ {{/each}} + {{/if}} + {{/if}} + {{!--
--}} +
+ +
+
+ +
diff --git a/templates/item/item-sheet.hbs b/templates/item/item-sheet.hbs new file mode 100644 index 0000000..6054cc2 --- /dev/null +++ b/templates/item/item-sheet.hbs @@ -0,0 +1,432 @@ +
+ + {{!-- SHEET HEADER --}} +
+
+
+ +
+
+
+ +
+
{{item.type}}
+
+
+
+ + {{!-- SHEET BODY --}} +
+ + {{#if (eq item.type "specialty")}} + {{!-- SPECIALTY ITEM SHEET --}} +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ + {{{editor system.description target="system.description" button=true editable=editable}}} +
+
+ + {{else if (eq item.type "weapon")}} + {{!-- WEAPON ITEM SHEET --}} +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + {{{editor system.description target="system.description" button=true editable=editable}}} +
+
+ + {{else if (eq item.type "armor")}} + {{!-- ARMOR ITEM SHEET --}} +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + {{{editor system.description target="system.description" button=true editable=editable}}} +
+
+ + {{else if (eq item.type "equipment")}} + {{#if system.isDrug}} + {{!-- DRUG ITEM SHEET --}} +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + {{{editor system.description target="system.description" button=true editable=editable}}} +
+
+ {{else}} + {{!-- EQUIPMENT / GENERIC ITEM SHEET --}} +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + {{{editor system.description target="system.description" button=true editable=editable}}} +
+
+ {{/if}} + {{else if (eq item.type "ebb")}} + {{!-- EBB FORMULA ITEM SHEET --}} +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+ + {{{editor system.description target="system.description" button=true editable=editable}}} +
+
+ + {{else}} +
+
+ + {{{editor system.description target="system.description" button=true editable=editable}}} +
+
+ {{/if}} + +
+ +
diff --git a/templates/item/modifiers-partial.hbs b/templates/item/modifiers-partial.hbs new file mode 100644 index 0000000..a52413a --- /dev/null +++ b/templates/item/modifiers-partial.hbs @@ -0,0 +1,43 @@ +
+ {{#each system.modifiers as | modifier |}} + + + + {{{@root.config.Icons.buttons.delete}}} + + {{else}} +
{{localize "FLBR.ItemNoModifier"}}
+ {{/each}} +