commit 2ebd5803fc7d8ea6f95ee3cd81245a5bba5a415e Author: Tim Evans Date: Mon Jun 1 23:19:46 2026 +0100 Initial commit — full system source + zero-engine-state.md reference diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..69b30c7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,29 @@ +# ── 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-*/ + +# 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..27a45ee --- /dev/null +++ b/README.md @@ -0,0 +1,136 @@ +
+

Blade Runner - The Roleplaying Game
🎲 Foundry VTT 🎲

+

+ Version + + Foundry Version + + + Forge Installs + + + Downloads + + + Maintenance + + + License: GPLv3 + + + Patreon + +

+ +> The **OFFICIAL** game system for the **Blade Runner RPG** on Foundry VTT! + +# 📦 What's in the Box? + +The core system features no content. It only provides support for various mechanics required to play the game: + +- Dice roller +- Actor sheets: + - Character (PC & NPC) + - Vehicle + - Loot (item pile) +- Item sheets: + - Generic + - Armor + - Weapon + - Explosive + - Specialty + - Synthetic Augment + - Critical Injury + +# 🚀 Install + +## System + +> **Note**: The game system is compatible with Foundry **V10** & **V11**. + +1. Go to the setup page in Foundry and choose `Game Systems` +2. Click the `Install System` and search for "Blade Runner", or paste in this [manifest link](https://github.com/fvtt-fria-ligan/blade-runner-foundry-vtt/releases/latest/download/system.json) +3. Create a `Game World` using the Blade Runner system and start it +4. Read the _Game System's Manual_ Journal Entry in the system's compedium +5. _Enjoy!_ + +## Premium Modules + +> **Note**: Premium modules are not mandatory to play the game as the game system is free to use, but they will save you _weeks_ of preparation. + +1. Get the premium modules at the [Free League's shop](https://freeleaguepublishing.com/en/store/?collection_id=405939749122) +2. Retrieve your codes in your mail box _(contact Free League if you did not recieve anything after 15 minutes, also check your spam box)_ +3. Activate the codes in your Foundry account: visit [foundryvtt.com](https://foundryvtt.com/), go to your account settings, in _"Purchased Content"_, and paste in the codes +4. Launch the Foundry client _(and make sure you have updated Foundry to V10)_ +5. Install the Blade Runner premium modules and some of the recommended third-party modules _(optional, see below)_ +6. Create a new World with the Blade Runner system and start it +7. In the options, activate the modules +8. Refresh and import the content. _Have fun!_ + +# 🧩 Supported Modules + +List of modules confirmed to be stable with the Blade Runner game system: + +- [Dice So Nice](https://foundryvtt.com/packages/dice-so-nice/): Enables the Blade Runner 3D dice. Also, a set of predefined Special Effects is available in the `/assets/plugins` folder. _(Use the import button in the DsN settings.)_ +- [Year Zero Engine: Combat](https://foundryvtt.com/packages/yze-combat): Combat mechanics with initiative cards. +- [Simultaneous Cards](https://foundryvtt.com/packages/simultaneous-cards): Useful for chases. +- [Simple Calendar](https://foundryvtt.com/packages/foundryvtt-simple-calendar): Tracks time, useful for the investigation's time table. +- [Item Piles](https://foundryvtt.com/packages/item-piles): Useful for creating merchants and RDU services providers. +- [Token Action HUD](https://foundryvtt.com/packages/token-action-hud-core) with [this plugin](https://foundryvtt.com/packages/token-action-hud-bladerunner) +- [Quick Insert - Search Widget](https://foundryvtt.com/packages/quick-insert): Quickly search content. + +List of premium content modules: + +- [Blade Runner RPG Core Rules](https://foundryvtt.com/packages/blade-runner-core-game) +- [Blade Runner RPG Starter Set](https://foundryvtt.com/packages/blade-runner-starter-set) +- [Blade Runner RPG Fiery Angels](https://foundryvtt.com/packages/blade-runner-fiery-angels) + +# 🛠️ Contributing + +If you want to contribute to the project, download and build it for something else, or if you simply have an issue, please read [our contributing guide](https://github.com/fvtt-fria-ligan/blade-runner-foundry-vtt/blob/main/CONTRIBUTING.md) to learn more about how we accept contributions and how to set up the development version of the project. + +## Translations + + +Translation status + + +# 👤 Author + +

+ + Stefouch Gaming Lab + +
+ Built by a fan, for the fans. +

+ +# 👷 Maintainer + +

+ + Hod Publishing + +

+ +# 🔗 See Also + +- [Foundry VTT](https://foundryvtt.com/) +- [Blade Runner - The Roleplaying Game](https://bladerunner-rpg.com/) +- [Free League Publishing: Blade Runner](https://freeleaguepublishing.com/en/games/blade-runner/) +- Discord: + - [The Foundry](https://discord.gg/foundryvtt) + - [Year Zero Worlds](https://discord.gg/RnaydHR) + - [Blade Runner RPG](https://discord.gg/fB9sUEsbgz) + +# 📜 List of Changes + +See the [changelog](https://github.com/fvtt-fria-ligan/blade-runner-foundry-vtt/blob/master/CHANGELOG.md#changelog) for a complete list of changes applied to the system since 2022. + +# 📝 Licenses + +- **Content & Logo:** [Blade Runner - The Roleplaying Game](https://freeleaguepublishing.com/en/games/blade-runner/) is © 2022 Alcon Entertainment, LLC. All rights reserved. The parts of this project protected under this copyright may not be distributed commercially or freely. This includes art, logo, and copyright text. +- **Source Code:** All source code _(javascript, hbs, scss, as well as system templates and the like)_ are Copyright © 2022 [Stefouch](https://github.com/Stefouch), and licenced under the [GNU General Public License v3.0](https://github.com/fvtt-fria-ligan/blade-runner-foundry-vtt/blob/master/LICENSE). +- **Foundry VTT:** The project is created following the Foundry VTT [Limited License Agreement for module development](https://foundryvtt.com/article/license/). +- **Icons:** Icons in the `.static/assets/icons/` folder are courtesy of [Game-icons.net](https://game-icons.net/) and licenced under [CC BY 3.0](https://creativecommons.org/licenses/by/3.0/). +- **Fonts:** The fonts used in this project carry their own licences. 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/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/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/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/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/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/blade-runner.css b/blade-runner.css new file mode 100644 index 0000000..2968ce8 --- /dev/null +++ b/blade-runner.css @@ -0,0 +1 @@ +@font-face{font-family:Blade Runner Symbols;src:url(./fonts/blade-runner-symbols.ttf)}.blade-runner-symbols{font-family:Blade Runner Symbols;font-size:1.3em;font-weight:400;line-height:0;position:relative;bottom:-.125em}@layer variables.themes.bladerunner{:root{--color-white: #FFF2DE;--color-black: #1D1D1B;--color-red: #C32E1B;--color-red-darker: #AB191E;--color-green: #A3BA8E;--color-blue-lighter: #A3CDD3;--color-blue: #74B8C0;--color-blue-darker: #065C71;--color-yellow: #F7BD59;--color-gold: #F9C254;--color-skin: #FFE5BD;--color-border--core-box-orange: #F3742A;--color-border--core-box-beige: rgba(255,229,189,.7);--color-border--core-box-blue: rgba(197,220,225,.7);--color-background--gray-dust: rgba(0,0,0,.4);--color-foreground: var(--color-white);--color-background: var(--color-black);--color-border: #74B8C0;--color-disabled: gray;--font-editor: "Caveat", "Goudy Old Style", "Times New Roman", serif;--font-text-editor: "Goudy Old Style", "Times New Roman", serif;--font-header: "URW DIN Condensed", sans-serif;--font-headline: "IdentikalSans", sans-serif;--font-table: "Foundry Gridnik", "Consolas", sans-serif;--font-size--extra-small: .7rem;--font-size--very-small: .8rem;--font-size--small: .9rem;--font-size--default: 1rem;--font-size--large: 1.5rem;--font-size--extra-large: 2rem;--color-text-hyperlink: var(--color-red-darker);--content-link-text-color: unset}}.blade-runner .flex{display:flex}.blade-runner .flex-0{flex:0}.blade-runner .flex-1{flex:1}.blade-runner .row{flex-direction:row}.blade-runner .column{flex-direction:column}.blade-runner .wrap{flex-wrap:wrap}.blade-runner .grow{flex-grow:1}.blade-runner .center{text-align:center}.blade-runner .align-center{align-items:center}.blade-runner .align-end{align-items:flex-end}.blade-runner .space-around{justify-content:space-around}.blade-runner .space-between{justify-content:space-between}.blade-runner .justify-start{justify-content:flex-start}.blade-runner .justify-center{justify-content:center}.blade-runner .justify-end{justify-content:flex-end}.blade-runner .flex-half{flex:0 0 50%}.blade-runner .margin{margin:4px}.blade-runner .padding{padding:4px}.blade-runner .full{width:100%;height:100%}.blade-runner .container{display:grid;grid-template-columns:1fr 1fr;gap:4px}.blade-runner .grid-column-all{grid-column:1/-1}.blade-runner .col-1{flex:0 0 auto;width:8%}.blade-runner .col-2{flex:0 0 auto;width:17%}.blade-runner .col-3{flex:0 0 auto;width:25%}.blade-runner .col-4{flex:0 0 auto;width:33%}.blade-runner .col-5{flex:0 0 auto;width:42%}.blade-runner .col-6{flex:0 0 auto;width:50%}.blade-runner .col-7{flex:0 0 auto;width:58%}.blade-runner .col-8{flex:0 0 auto;width:67%}.blade-runner .col-9{flex:0 0 auto;width:75%}.blade-runner .col-10{flex:0 0 auto;width:83%}.blade-runner .col-11{flex:0 0 auto;width:92%}.blade-runner .col-12{flex:0 0 auto;width:100%}.switch{position:relative;display:inline-block;width:30px;height:18px}.switch input{opacity:0;width:0;height:0}.switch .slider{position:absolute;cursor:pointer;inset:0;outline:1px solid var(--color-blue);-webkit-transition:.4s;transition:.4s}.switch .slider.red{outline-color:var(--color-red-darker)}.switch .slider:before{position:absolute;content:"";height:12px;width:12px;left:3px;bottom:3px;background-color:var(--color-blue);-webkit-transition:.4s;transition:.4s}.switch .slider.red:before{background-color:var(--color-red-darker)}.switch input:checked+.slider{outline-color:var(--color-green)}.switch input:checked+.slider.gold{outline-color:var(--color-gold)}.switch input:focus+.slider{box-shadow:0 0 1px var(--color-shadow-highlight)}.switch input:checked+.slider:before{background-color:var(--color-green);-webkit-transform:translateX(12px);-ms-transform:translateX(12px);transform:translate(12px)}.switch input:checked+.slider.gold:before{background-color:var(--color-gold)}.switch .slider.round{border-radius:12px}.switch .slider.round:before{border-radius:50%}#sidebar-tabs .fa-comments:before{content:"\f8ef"}#sidebar-tabs .fa-swords:before{content:"\e19b"}#context-menu li.context-item{padding:5px;line-height:1.1}#players h3{font-size:unset}#logo{content:url(./assets/icons/blade-runner-failure.svg);width:100px;height:100px}#pause>img{content:url(./assets/logos/lapd_br_badge.webp);width:136.4px;height:200px;top:-50px;left:calc(50% - 68.2px);opacity:.7;--fa-animation-duration: 42s}#pause figcaption{font-family:var(--font-header)}h1,h2,h3,h4,h5,h6{text-decoration:none;border:none;margin:0}h2{font-size:var(--font-size--large)}h3{font-size:var(--font-size--default)}h4{font-size:var(--font-size--small)}h5{font-size:var(--font-size--very-small)}h6{font-size:var(--font-size--extra-small)}a.entity-link,a.content-link,a.inline-roll,a.inline-table{border:none;background:transparent;border-bottom:1px dotted gray;padding:0;text-indent:0;border-radius:2px;color:unset}a.entity-link i,a.content-link i,a.inline-roll i,a.inline-table i{color:var(--color-text-dark-inactive);margin-right:.25em}a.entity-link img,a.content-link img,a.inline-roll img,a.inline-table img{display:inline}a.entity-link.broken,a.content-link.broken,a.inline-roll.broken,a.inline-table.broken{color:var(--color-red);background:rgba(255,0,0,.05);border:1px dashed var(--color-red)}a.content-link{white-space:initial;word-break:keep-all}a.entity-link{cursor:help;user-select:text}code,code.block{color:unset}.blade-runner .capacity-boxes{display:block;color:var(--color-green);white-space:nowrap;line-height:0}.blade-runner .boxed-value{border:1px solid var(--color-border);background-color:var(--color-black);padding:4px;background-image:url(./assets/textures/character-sheet-editor.webp);background-size:100% 100%;border-radius:4px;color:var(--color-black);font-family:var(--font-table);font-weight:700;text-align:center}.blade-runner .boxed-property{border:1px solid var(--color-border);border-radius:0;background-color:var(--color-black);padding:4px}.blade-runner .boxed-property p{color:var(--color-blue-lighter)}.blade-runner button:not(.tox-tbtn),.blade-runner .button,.blade-runner .dialog-button,.app:not(.form) .application:not(.form) button:not(.tox-tbtn),.app:not(.form) .application:not(.form) .button,.app:not(.form) .application:not(.form) .dialog-button{border:1px solid var(--color-border);background-color:var(--color-black);padding:4px;border-radius:4px;font-family:var(--font-table);font-weight:700;color:var(--color-green);letter-spacing:-1px;cursor:pointer;transition:color .25s linear,background-color .25s ease}.blade-runner button:not(.tox-tbtn):hover,.blade-runner .button:hover,.blade-runner .dialog-button:hover,.app:not(.form) .application:not(.form) button:not(.tox-tbtn):hover,.app:not(.form) .application:not(.form) .button:hover,.app:not(.form) .application:not(.form) .dialog-button:hover{background:#333;border-color:var(--color-yellow);color:var(--color-red)}.blade-runner button:not(.tox-tbtn):hover i,.blade-runner .button:hover i,.blade-runner .dialog-button:hover i,.app:not(.form) .application:not(.form) button:not(.tox-tbtn):hover i,.app:not(.form) .application:not(.form) .button:hover i,.app:not(.form) .application:not(.form) .dialog-button:hover i{color:var(--color-yellow)}.blade-runner button:not(.tox-tbtn):disabled,.blade-runner button:not(.tox-tbtn).disabled,.blade-runner .button:disabled,.blade-runner .button.disabled,.blade-runner .dialog-button:disabled,.blade-runner .dialog-button.disabled,.app:not(.form) .application:not(.form) button:not(.tox-tbtn):disabled,.app:not(.form) .application:not(.form) button:not(.tox-tbtn).disabled,.app:not(.form) .application:not(.form) .button:disabled,.app:not(.form) .application:not(.form) .button.disabled,.app:not(.form) .application:not(.form) .dialog-button:disabled,.app:not(.form) .application:not(.form) .dialog-button.disabled{border-color:var(--color-disabled);color:var(--color-disabled)}.blade-runner button.default,.app:not(.form) .application:not(.form) button.default{background:var(--color-background)}.app.window-app.dialog,.app.window-app.blade-runner,.application.window-app.dialog,.application.window-app.blade-runner{color:var(--color-foreground);border:1px solid var(--color-blue-darker);border-radius:4px;background-image:url(./assets/textures/character-sheet-background.webp);background-position:top center;background-size:cover;background-repeat:no-repeat;background-color:var(--color-background)}.app.window-app.dialog .editor,.app.window-app.blade-runner .editor,.application.window-app.dialog .editor,.application.window-app.blade-runner .editor{border:2px solid var(--color-border);border-radius:0;background-image:url(./assets/textures/character-sheet-editor.webp);background-size:100% 100%;color:var(--color-black);font-family:var(--font-editor);margin-top:4px;padding:4px}.app.window-app.dialog .editor p,.app.window-app.dialog .editor li,.app.window-app.blade-runner .editor p,.app.window-app.blade-runner .editor li,.application.window-app.dialog .editor p,.application.window-app.dialog .editor li,.application.window-app.blade-runner .editor p,.application.window-app.blade-runner .editor li{color:var(--color-black);font-size:var(--font-size--default)}.app.window-app.dialog input:not([type=range]),.app.window-app.dialog select,.app.window-app.blade-runner input:not([type=range]),.app.window-app.blade-runner select,.application.window-app.dialog input:not([type=range]),.application.window-app.dialog select,.application.window-app.blade-runner input:not([type=range]),.application.window-app.blade-runner select{background-image:url(./assets/textures/character-sheet-editor.webp);background-size:100% 100%;color:var(--color-black);font-family:var(--font-table);font-weight:700;text-align:center}.app.window-app.dialog input:not([type=range]):disabled,.app.window-app.dialog input:not([type=range]).disabled,.app.window-app.dialog select:disabled,.app.window-app.dialog select.disabled,.app.window-app.blade-runner input:not([type=range]):disabled,.app.window-app.blade-runner input:not([type=range]).disabled,.app.window-app.blade-runner select:disabled,.app.window-app.blade-runner select.disabled,.application.window-app.dialog input:not([type=range]):disabled,.application.window-app.dialog input:not([type=range]).disabled,.application.window-app.dialog select:disabled,.application.window-app.dialog select.disabled,.application.window-app.blade-runner input:not([type=range]):disabled,.application.window-app.blade-runner input:not([type=range]).disabled,.application.window-app.blade-runner select:disabled,.application.window-app.blade-runner select.disabled{color:var(--color-disabled)}.app.window-app.dialog select,.app.window-app.dialog img,.app.window-app.blade-runner select,.app.window-app.blade-runner img,.application.window-app.dialog select,.application.window-app.dialog img,.application.window-app.blade-runner select,.application.window-app.blade-runner img{border:none}.app.window-app.dialog img,.app.window-app.blade-runner img,.application.window-app.dialog img,.application.window-app.blade-runner img{max-height:100%}.app.window-app.dialog label,.app.window-app.blade-runner label,.application.window-app.dialog label,.application.window-app.blade-runner label{color:var(--color-blue-lighter)}.app.window-app.dialog form .notes,.app.window-app.dialog form .hint,.app.window-app.blade-runner form .notes,.app.window-app.blade-runner form .hint,.application.window-app.dialog form .notes,.application.window-app.dialog form .hint,.application.window-app.blade-runner form .notes,.application.window-app.blade-runner form .hint{color:var(--color-green)}.app.window-app.dialog .dialog-button,.app.window-app.blade-runner .dialog-button,.application.window-app.dialog .dialog-button,.application.window-app.blade-runner .dialog-button{border:1px solid var(--color-border);background-color:var(--color-black);padding:4px;border-radius:4px;font-family:var(--font-table);font-weight:700;color:var(--color-green);letter-spacing:-1px;cursor:pointer;transition:color .25s linear,background-color .25s ease}.app.window-app.dialog .dialog-button:hover,.app.window-app.blade-runner .dialog-button:hover,.application.window-app.dialog .dialog-button:hover,.application.window-app.blade-runner .dialog-button:hover{background:#333;border-color:var(--color-yellow);color:var(--color-red)}.app.window-app.dialog .dialog-button:hover i,.app.window-app.blade-runner .dialog-button:hover i,.application.window-app.dialog .dialog-button:hover i,.application.window-app.blade-runner .dialog-button:hover i{color:var(--color-yellow)}.app.window-app.dialog .dialog-button:disabled,.app.window-app.dialog .dialog-button.disabled,.app.window-app.blade-runner .dialog-button:disabled,.app.window-app.blade-runner .dialog-button.disabled,.application.window-app.dialog .dialog-button:disabled,.application.window-app.dialog .dialog-button.disabled,.application.window-app.blade-runner .dialog-button:disabled,.application.window-app.blade-runner .dialog-button.disabled{border-color:var(--color-disabled);color:var(--color-disabled)}.app.window-app.dialog button.default,.app.window-app.blade-runner button.default,.application.window-app.dialog button.default,.application.window-app.blade-runner button.default{background:var(--color-background)}.app.window-app.dialog h1,.app.window-app.dialog h2,.app.window-app.dialog h3,.app.window-app.dialog h4,.app.window-app.dialog h5,.app.window-app.dialog h6,.app.window-app.blade-runner h1,.app.window-app.blade-runner h2,.app.window-app.blade-runner h3,.app.window-app.blade-runner h4,.app.window-app.blade-runner h5,.app.window-app.blade-runner h6,.application.window-app.dialog h1,.application.window-app.dialog h2,.application.window-app.dialog h3,.application.window-app.dialog h4,.application.window-app.dialog h5,.application.window-app.dialog h6,.application.window-app.blade-runner h1,.application.window-app.blade-runner h2,.application.window-app.blade-runner h3,.application.window-app.blade-runner h4,.application.window-app.blade-runner h5,.application.window-app.blade-runner h6{color:var(--color-blue);font-family:var(--font-header);letter-spacing:1px}.app.window-app.dialog h1,.app.window-app.dialog h2,.app.window-app.dialog h3,.app.window-app.dialog h4,.app.window-app.blade-runner h1,.app.window-app.blade-runner h2,.app.window-app.blade-runner h3,.app.window-app.blade-runner h4,.application.window-app.dialog h1,.application.window-app.dialog h2,.application.window-app.dialog h3,.application.window-app.dialog h4,.application.window-app.blade-runner h1,.application.window-app.blade-runner h2,.application.window-app.blade-runner h3,.application.window-app.blade-runner h4{text-transform:uppercase}.app.window-app.dialog .rollable,.app.window-app.blade-runner .rollable,.application.window-app.dialog .rollable,.application.window-app.blade-runner .rollable{cursor:pointer}.app.window-app.dialog .rollable:hover,.app.window-app.blade-runner .rollable:hover,.application.window-app.dialog .rollable:hover,.application.window-app.blade-runner .rollable:hover{text-shadow:0 0 8px var(--color-shadow-primary);box-shadow:0 0 8px var(--color-shadow-primary)}.app.window-app.dialog .window-header,.app.window-app.blade-runner .window-header,.application.window-app.dialog .window-header,.application.window-app.blade-runner .window-header{border:none}.app.window-app.dialog .window-header a.header-button,.app.window-app.blade-runner .window-header a.header-button,.application.window-app.dialog .window-header a.header-button,.application.window-app.blade-runner .window-header a.header-button{display:flex;flex:none;flex-flow:row-reverse;max-width:24px;height:32px;white-space:nowrap;line-height:2;overflow:hidden;padding:0 4px;margin:0 0 0 2px;color:transparent;transition:max-width 1s ease}.app.window-app.dialog .window-header a.header-button:hover,.app.window-app.blade-runner .window-header a.header-button:hover,.application.window-app.dialog .window-header a.header-button:hover,.application.window-app.blade-runner .window-header a.header-button:hover{max-width:200px;color:var(--color-blue)}.app.window-app.dialog .window-header a.header-button>i,.app.window-app.blade-runner .window-header a.header-button>i,.application.window-app.dialog .window-header a.header-button>i,.application.window-app.blade-runner .window-header a.header-button>i{font-size:var(--font-size--small);color:var(--color-blue);margin:8px 0 0 8px}.app.window-app.dialog .window-content,.app.window-app.blade-runner .window-content,.application.window-app.dialog .window-content,.application.window-app.blade-runner .window-content{color:var(--color-foreground);padding:4px;background:none;overflow:auto}.app.window-app.dialog .window-content .sheet-tabs,.app.window-app.blade-runner .window-content .sheet-tabs,.application.window-app.dialog .window-content .sheet-tabs,.application.window-app.blade-runner .window-content .sheet-tabs{display:flex;justify-content:space-around;font-family:var(--font-header);font-weight:700;text-transform:uppercase;padding:4px;margin-top:4px}.app.window-app.dialog .window-content .sheet-tab,.app.window-app.blade-runner .window-content .sheet-tab,.application.window-app.dialog .window-content .sheet-tab,.application.window-app.blade-runner .window-content .sheet-tab{color:var(--color-blue)}.app.window-app.dialog .window-content .sheet-tab.active,.app.window-app.blade-runner .window-content .sheet-tab.active,.application.window-app.dialog .window-content .sheet-tab.active,.application.window-app.blade-runner .window-content .sheet-tab.active{color:var(--color-red);border-bottom:2px solid var(--color-red)}.blade-runner.dialog.update-log .boxed-property{margin:4px 0}.blade-runner.dialog.update-log .boxed-property h1,.blade-runner.dialog.update-log .boxed-property h2,.blade-runner.dialog.update-log .boxed-property h3{color:var(--color-gold)}.blade-runner.dialog.update-log .boxed-property h4{color:var(--color-green)}.blade-runner.dialog.update-log .update-document-type{color:var(--color-blue-lighter)}.blade-runner.dialog.update-log .update-document-description{color:#ccc}.blade-runner.dialog.update-log .notification{display:block}.blade-runner ::-webkit-scrollbar{width:8px;height:8px}.blade-runner ::-webkit-scrollbar-thumb{border-radius:0;border:1px solid var(--color-black);background-color:var(--color-blue-lighter)}#sidebar #sidebar-tabs{border-bottom:1px solid var(--color-blue-lighter);box-shadow:0 0 10px var(--color-blue)}#sidebar #sidebar-tabs>.item{color:var(--color-blue-lighter)}#sidebar #sidebar-tabs>.item.active{color:var(--color-yellow);border:1px solid var(--color-blue);border-bottom:none;box-shadow:0 0 6px inset var(--color-blue-lighter)}#sidebar.collapsed #sidebar-tabs>.item.active{border-bottom:1px solid var(--color-blue-lighter)}#sidebar .action-buttons button,.sidebar-popout .action-buttons button{line-height:1}#chat-form textarea{border:1px solid var(--color-border);border-radius:0;background-image:url(./assets/textures/character-sheet-editor.webp);background-size:100% 100%}.chat-message{font-family:var(--font-table);border:1px solid var(--color-blue-lighter);border-radius:0;background-color:var(--color-background);background-image:url(./assets/textures/character-sheet-background.webp);background-position:top center;background-size:cover;background-repeat:no-repeat;color:var(--color-blue-lighter)}.chat-message.message.whisper{border:1px solid var(--color-skin);background-image:url(./assets/textures/chat-card-background.webp);background-position:top center;background-size:cover;background-repeat:no-repeat;color:var(--color-skin)}.chat-message h1,.chat-message h2,.chat-message h3,.chat-message h4,.chat-message h5,.chat-message h6{font-family:var(--font-table);color:var(--color-red)}.chat-message h1,.chat-message h2,.chat-message h3{font-weight:700;letter-spacing:1px}.chat-message img{border:none;max-height:100px}.chat-message hr{border:none;border-top:1px solid var(--color-blue)}.chat-message a.content-link,.chat-message a.inline-roll,.chat-message a.inline-table{border:1px solid var(--color-blue-darker);border-radius:4px/12px;background-color:var(--color-black);font-weight:700;color:var(--color-green);letter-spacing:-1px;padding:2px 6px 0}.chat-message a.content-link i,.chat-message a.inline-roll i,.chat-message a.inline-table i{color:inherit}.chat-message a.content-link:hover,.chat-message a.inline-roll:hover,.chat-message a.inline-table:hover{border-color:var(--color-yellow);color:var(--color-red)}.chat-message a.content-link:hover i,.chat-message a.inline-roll:hover i,.chat-message a.inline-table:hover i{color:var(--color-yellow)}.chat-message a.nue-tab,.chat-message a.nue-action,.chat-message a[href]{color:var(--color-blue)}.chat-message a.button,.chat-message button{color:var(--color-green)}.chat-message .message-header{color:var(--color-disabled)}.chat-message .message-metadata{opacity:0}.chat-message:hover .message-metadata{opacity:1;transition:opacity .5s ease}.chat-message .photo{margin-top:10px}.chat-message .details{display:grid;grid-template-columns:auto 1fr;column-gap:16px;color:var(--color-green)}.chat-message .details .modifiers{color:var(--color-blue)}.chat-message .details .special{text-shadow:-1px -1px 0 #000,1px -1px 0 #000,-1px 1px 0 #000,1px 1px 0 #000;color:var(--color-red)}.chat-message fieldset.item-attack{background-color:var(--color-black);border:1px solid var(--color-blue);margin:4px 0 0}.chat-message .chat-card-footer{color:var(--color-white);font-size:var(--font-size--extra-small);font-weight:400}.chat-message .actor-damage .broken{color:var(--color-red-darker);font-weight:700}.chat-message .actor-damage .notes,.chat-message .actor-damage .hint{font-weight:400;line-height:1}.chat-message .yzur .d6bladerunnerdie.min,.chat-message .yzur .d8bladerunnerdie.min,.chat-message .yzur .d10bladerunnerdie.min,.chat-message .yzur .d12bladerunnerdie.min{font-family:Blade Runner Symbols}.chat-message .yzur .dice-flavor{color:unset;text-align:left}.chat-message .yzur .dice-formula,.chat-message .yzur .dice-total{border-right:1px solid var(--color-border);border-left:1px solid var(--color-border);background-color:#111;box-shadow:none;padding:4px}.chat-message .yzur .dice-formula{border-top:1px solid var(--color-border);border-bottom:none;margin:0}.chat-message .yzur .dice-total{border-top:none;border-bottom:1px solid var(--color-border);color:var(--color-disabled)}.chat-message .yzur .dice-result{color:var(--color-blue);cursor:pointer}.chat-message .yzur .success{color:var(--color-green)}.chat-message .yzur .failure{color:var(--color-red)}.chat-message .yzur .tooltip-parts{display:flex;flex-direction:row;flex-wrap:wrap;justify-content:space-evenly;gap:4px;padding:10px;background-color:var(--color-background);border-left:1px dashed var(--color-disabled);border-right:1px dashed var(--color-disabled)}.chat-message .yzur .part-header .part-formula{display:none}.chat-message .yzur .part-header .part-flavor{color:inherit}.chat-message .yzur .part-header .part-total{color:var(--color-disabled)}.chat-message .yzur .part-header .part-total.success{color:var(--color-green)}.chat-message .yzur .dice-rolls .roll.pushed{color:#555;filter:none}.chat-message .yzur .dice-rolls .roll.discarded:not(.pushed){opacity:0}.chat-message .yzur .dice-infos{background-color:var(--color-black);border:1px solid var(--color-blue);margin:4px 0 0}.chat-message .yzur .dice-info-grid{display:grid;grid-template-columns:auto 1fr;column-gap:16px;color:var(--color-green);border:var(--color-blue)}.chat-message .yzur .roll-controls{display:flex;flex-direction:row;justify-content:space-around;gap:4px;margin-top:4px}.chat-message .yzur .roll-button{flex-grow:1;font-size:var(--font-size--extra-small);line-height:1}.chat-message .yzur .roll-button:not(:disabled){border:1px solid var(--color-blue)}.app.sheet .editor:not(.prosemirror),.application.sheet .editor:not(.prosemirror){min-height:55px;height:100%}.app.sheet .editor:not(.prosemirror) .editor-content,.application.sheet .editor:not(.prosemirror) .editor-content{height:100%}.app.sheet .editor:not(.prosemirror) .tox-tinymce,.application.sheet .editor:not(.prosemirror) .tox-tinymce{min-height:200px;height:100%}.flbr-enriched-actor{display:grid;grid-template-columns:1fr 1fr;grid-template-areas:"title title" "description description" "stats img"}.flbr-enriched-actor h3{grid-area:title}.flbr-enriched-actor .actor-description{grid-area:description}.flbr-enriched-actor .actor-stats{grid-area:stats}.flbr-enriched-actor img{grid-area:img;align-self:center;justify-self:center}.application.journal-entry .journal-entry-content .flbr-enriched-actor{margin-bottom:20px;border:1px solid var(--color-border--core-box-blue);background-color:#0006}.application.journal-entry .journal-entry-content .flbr-enriched-actor>*:not(img){padding:25px}.application.journal-entry .journal-entry-content .flbr-enriched-actor h3{font-family:var(--font-gridnik);font-weight:700;color:var(--color-blue-lighter);border-bottom:1px solid var(--color-border--core-box-blue)}.application.journal-entry .journal-entry-content .flbr-enriched-actor .actor-description{border-bottom:1px solid var(--color-border--core-box-blue);background-image:url(./assets/textures/page-text-zone-background-blue-lighter.webp);background-position:top center;background-size:cover}.application.journal-entry .journal-entry-content .flbr-enriched-actor .actor-description p{font-family:var(--font-table);color:var(--color-green)}.application.journal-entry .journal-entry-content .flbr-enriched-actor .actor-stats{border-right:1px solid var(--color-border--core-box-blue)}.application.journal-entry .journal-entry-content .flbr-enriched-actor table{font-family:var(--font-table);color:var(--color-blue-lighter);border:none}.application.journal-entry .journal-entry-content .flbr-enriched-actor table tbody{vertical-align:top}.application.journal-entry .journal-entry-content .flbr-enriched-actor table tr{background:none}.application.journal-entry .journal-entry-content .flbr-enriched-actor table tr:not(:first-child){border-top:2px solid var(--color-green)}.application.journal-entry .journal-entry-content .flbr-enriched-actor table tr td:first-of-type{font-weight:700}.application.journal-entry .journal-entry-content .flbr-enriched-actor table tr td:nth-child(2){font-size:1.125em;text-align:right;letter-spacing:.1em;word-spacing:.2em}.application.journal-entry .journal-entry-content .flbr-enriched-handout{width:95%}.application.journal-entry .journal-entry-content .flbr-enriched-handout img{align-self:flex-start;background-color:var(--color-background--gray-dust)}.chat-message .flbr-enriched-actor{grid-template-columns:1fr;grid-template-areas:"title" "img" "description" "stats"}.chat-message .flbr-enriched-actor .actor-stats{display:none}.chat-message .flbr-enriched-actor img{max-height:150px;margin-top:10px}.chat-message .flbr-enriched-handout{display:flex;flex-direction:column}.chat-message .flbr-enriched-handout img{align-self:center;max-height:initial;margin-top:10px;border:1px solid var(--color-border--core-box-blue)}.blade-runner.sheet.actor h1 a.btn,.blade-runner.sheet.actor h2 a.btn,.blade-runner.sheet.actor h3 a.btn,.blade-runner.sheet.actor h4 a.btn{float:right}.blade-runner.sheet.actor .capacities{display:flex;flex-direction:column;gap:4px}.blade-runner.sheet.actor .capacities .half{border-color:var(--color-yellow)}.blade-runner.sheet.actor .capacities .half h3,.blade-runner.sheet.actor .capacities .half .capacity-boxes{color:var(--color-yellow)}.blade-runner.sheet.actor .capacities .broken{border-color:var(--color-red-darker)}.blade-runner.sheet.actor .capacities .broken h3,.blade-runner.sheet.actor .capacities .broken .capacity-boxes{color:var(--color-red-darker)}.blade-runner.sheet.actor .capacities .capacity-rating{font-size:var(--font-size--large);float:right}.blade-runner.sheet.actor .capacities .loss{color:#424242}.blade-runner.sheet.actor .actions{font-family:var(--font-table);font-size:var(--font-size--very-small);font-weight:700;color:var(--color-green)}.blade-runner.sheet.actor .actions h4{grid-column:1/-1}.blade-runner.sheet.actor .actions a{padding:0 4px;border:1px solid transparent;border-radius:2px;white-space:nowrap}.blade-runner.sheet.actor .actions a i{opacity:0;width:0;transition:width .25s ease,opacity 0s}.blade-runner.sheet.actor .actions a:hover{border-color:var(--color-red);color:var(--color-red)}.blade-runner.sheet.actor .actions a:hover i{opacity:1;width:15px;transition:width .25s ease,opacity .25s ease .1ms}.blade-runner.sheet.actor .sheet-body{padding-top:4px}.blade-runner.sheet.character{min-width:525px;min-height:250px}.blade-runner.sheet.character label{color:var(--color-blue)}.blade-runner.sheet.character .sheet-header .container-header{display:grid;grid-template-columns:200px 1fr 50px;grid-template-rows:auto auto auto 1fr;grid-template-areas:"avatar name archetype" "avatar nature archetype" "avatar extra archetype" "avatar extra archetype";gap:4px}.blade-runner.sheet.character .sheet-header .avatar{grid-area:avatar;border:2px solid var(--color-border);border-radius:0;background-color:var(--color-black);padding:0;display:flex;align-items:center;justify-content:center;width:200px;height:200px}.blade-runner.sheet.character .sheet-header .actor-name{grid-area:name}.blade-runner.sheet.character .sheet-header .actor-name input{border:2px solid var(--color-border);border-radius:0;color:var(--color-blue-darker);font-family:var(--font-table);font-size:var(--font-size--default);text-transform:uppercase;letter-spacing:2px;width:100%;height:auto;padding:4px;margin:0}.blade-runner.sheet.character .sheet-header .archetype{grid-area:archetype}.blade-runner.sheet.character .sheet-header .archetype input{text-shadow:-1px -1px 0 #000,1px -1px 0 #000,-1px 1px 0 #000,1px 1px 0 #000;color:var(--color-blue);font-family:var(--font-table);font-size:var(--font-size--large);font-weight:700;text-transform:uppercase;text-align:left;width:200px;background:none;border:none;transform:rotate(90deg);transform-origin:25px;position:absolute;top:45px}.blade-runner.sheet.character .sheet-header .capacities{grid-area:extra}.blade-runner.sheet.character .sheet-body .stats-tab{display:grid;grid-template-columns:1fr 1fr}.blade-runner.sheet.character .sheet-body .stats-tab .stat-blok{border:1px solid var(--color-border);background-color:var(--color-black);padding:0 0 2px;display:grid;grid-template-columns:1fr auto 60px}.blade-runner.sheet.character .sheet-body .stats-tab .stat-blok .die-rating{border-bottom:1px solid var(--color-border);grid-column:1/3;text-align:right;padding-right:5px}.blade-runner.sheet.character .sheet-body .stats-tab .stat-blok .die-size{border-bottom:1px solid var(--color-border);border-left:1px solid var(--color-border);text-align:center}.blade-runner.sheet.character .sheet-body .stats-tab .stat-blok a{font-family:var(--font-table);color:var(--color-green);font-weight:700}.blade-runner.sheet.character .sheet-body .stats-tab .stat-blok a:hover{color:var(--color-red)}.blade-runner.sheet.character .sheet-body .stats-tab .stat-blok .attribute-name,.blade-runner.sheet.character .sheet-body .stats-tab .stat-blok .skill-name{display:flex;align-items:center}.blade-runner.sheet.character .sheet-body .stats-tab .stat-blok .attribute-name{text-transform:uppercase;border-bottom:1px solid var(--color-border);padding-left:8px}.blade-runner.sheet.character .sheet-body .stats-tab .stat-blok .skill-name{margin-left:4px;padding-left:4px}.blade-runner.sheet.character .sheet-body .stats-tab .stat-blok .skill-name:not(:nth-child(12)){border-bottom:1px dotted var(--color-green)}.blade-runner.sheet.character .sheet-body .stats-tab .stat-blok .skill-name.last{border:none}.blade-runner.sheet.character .sheet-body .stats-tab .stat-blok .attribute-score,.blade-runner.sheet.character .sheet-body .stats-tab .stat-blok .attribute-size{border-bottom:1px solid var(--color-border)}.blade-runner.sheet.character .sheet-body .stats-tab .stat-blok .attribute-score,.blade-runner.sheet.character .sheet-body .stats-tab .stat-blok .attribute-size,.blade-runner.sheet.character .sheet-body .stats-tab .stat-blok .skill-score,.blade-runner.sheet.character .sheet-body .stats-tab .stat-blok .skill-size{padding:2px 4px 2px 0}.blade-runner.sheet.character .sheet-body .mods-tab{display:grid;gap:4px}.blade-runner.sheet.character .sheet-body .combat-tab .weapons{grid-area:1/1/3/2}.blade-runner.sheet.character .sheet-body .combat-tab .injuries{grid-area:2/2/3/3}.blade-runner.sheet.character .sheet-body .combat-tab .actions{grid-column:1/-1;display:grid;grid-template-columns:repeat(4,1fr)}.blade-runner.sheet.character .sheet-body .combat-tab .weapons .embedded-items+.embedded-items{margin-top:4px}.blade-runner.sheet.character .sheet-body .combat-tab .weapons .embedded-items+.embedded-items .empty-inventory{display:none}.blade-runner.sheet.character .sheet-body .inventory-tab .special-gear{margin-bottom:4px}.blade-runner.sheet.character .sheet-body .inventory-tab .special-gear input{font-family:var(--font-editor);font-size:var(--font-size--default);font-weight:400;text-align:left}.blade-runner.sheet.character .sheet-body .inventory-tab .inventory{margin-bottom:4px}.blade-runner.sheet.character .sheet-body .inventory-tab .meta-currencies{display:grid;grid-template-columns:1fr 1fr 1fr;gap:4px}.blade-runner.sheet.character .sheet-body .bio-tab .bio-notes{grid-column:1/-1}.blade-runner.sheet.character .sheet-body .bio-tab .years-on-the-force{grid-column:1/-1;display:flex;flex-direction:row;align-items:center;width:100%}.blade-runner.sheet.character .sheet-body .bio-tab .years-on-the-force h3,.blade-runner.sheet.character .sheet-body .bio-tab .years-on-the-force h4{width:100%}.blade-runner.sheet.character .sheet-body .bio-tab .years-on-the-force input{width:69px}.blade-runner.sheet.vehicle{min-width:330px;min-height:328px}.blade-runner.sheet.vehicle .container-header{display:grid;gap:4px;grid-template-columns:2fr 1fr auto;grid-template-rows:auto auto auto;grid-template-areas:"name name type" "avatar stats type" "crew crew crew"}.blade-runner.sheet.vehicle .vehicle-type{grid-area:type;color:var(--color-blue);font-family:var(--font-table);font-size:var(--font-size--large);font-weight:700;text-transform:uppercase;writing-mode:vertical-lr;text-align:center}.blade-runner.sheet.vehicle .vehicle-name{grid-area:name}.blade-runner.sheet.vehicle .vehicle-name input{border:2px solid var(--color-border);border-radius:0;color:var(--color-blue-darker);font-family:var(--font-table);font-size:var(--font-size--default);text-transform:uppercase;letter-spacing:2px;width:100%;height:auto;padding:4px;margin:0}.blade-runner.sheet.vehicle .avatar{grid-area:avatar;border:1px solid var(--color-border);border-radius:0;background-color:var(--color-black);padding:0;display:flex;align-items:center;justify-content:center}.blade-runner.sheet.vehicle .vehicle-stats{grid-area:stats;display:flex;flex-direction:column;gap:4px}.blade-runner.sheet.vehicle .stat-blok{border:1px solid var(--color-border);border-radius:0;background-color:var(--color-black);padding:1px;display:grid;grid-template-columns:1fr auto 50px}.blade-runner.sheet.vehicle .stat-blok .attribute-name{display:flex;align-items:center;padding-left:2px}.blade-runner.sheet.vehicle .stat-blok .attribute-score,.blade-runner.sheet.vehicle .stat-blok .attribute-size{padding:2px 4px 2px 0}.blade-runner.sheet.vehicle .vehicle-properties{height:100%;display:flex;flex-direction:column}.blade-runner.sheet.vehicle .vehicle-properties hr{border-top-color:var(--color-green);border-bottom:none;width:100%}.blade-runner.sheet.vehicle .vehicle-property{display:flex;align-items:center;justify-content:space-between}.blade-runner.sheet.vehicle .vehicle-property label{flex:1;font-size:80%}.blade-runner.sheet.vehicle .vehicle-property select,.blade-runner.sheet.vehicle .vehicle-property input{flex:0 0 80px;font-size:90%;max-width:80px;height:20px}.blade-runner.sheet.vehicle label{color:var(--color-green);font-family:var(--font-table);font-size:var(--font-size--very-small);font-weight:700;text-transform:uppercase}.blade-runner.sheet.vehicle a.mvr-roll,.blade-runner.sheet.vehicle a.roll-vehicle-armor{font-family:var(--font-table);color:var(--color-green);font-weight:700}.blade-runner.sheet.vehicle a.mvr-roll:hover,.blade-runner.sheet.vehicle a.roll-vehicle-armor:hover{color:var(--color-red)}.blade-runner.sheet.vehicle .mounted-weapons{height:fit-content}.blade-runner.sheet.vehicle .vehicle-components{display:flex;flex-direction:row;gap:4px}.blade-runner.sheet.vehicle .vehicle-component{display:flex;align-items:center;gap:4px;padding:4px;background-color:#222;border-radius:4px}.blade-runner.sheet.vehicle .vehicle-component.disabled a{color:var(--color-red-darker)}.blade-runner.sheet.vehicle .vehicle-crew{grid-area:crew;display:flex;flex-direction:row;flex-wrap:wrap}.blade-runner.sheet.vehicle .vehicle-altitude{display:flex;flex-direction:row;align-items:center;justify-content:space-between}.blade-runner.sheet.vehicle .vehicle-altitude input{width:69px}.blade-runner.sheet.vehicle .combat-options{display:grid;gap:4px}.blade-runner.sheet.vehicle .actions{display:grid;grid-template-columns:1fr 1fr}.blade-runner.sheet.vehicle .active-effects{grid-column:1/-1}.blade-runner.sheet.vehicle .embedded-items{height:100%}.blade-runner.sheet.vehicle .embedded-item,.blade-runner.sheet.vehicle .embedded-item .details{width:100%}.blade-runner.sheet.vehicle .vehicle-inventory .embedded-item .photo{width:36px;height:36px;min-width:36px;min-height:36px}.vehicle-crew{display:flex;flex-direction:row;gap:4px}.vehicle-crew h4{font-size:small;writing-mode:vertical-lr;text-align:center;transform:rotate(180deg)}.vehicle-crew .vehicle-seat{position:relative;display:flex;align-items:center;justify-content:center;width:50px;min-width:50px;height:50px;border:1px solid var(--color-gold);border-radius:4px}.vehicle-crew .vehicle-seat img{border-radius:4px}.vehicle-crew .vehicle-seat .seat-icon{font-size:2em;text-align:center;vertical-align:middle}.vehicle-crew .vehicle-seat .seat-icon.broken{position:absolute;opacity:.5}.vehicle-crew .vehicle-seat.broken{border:1px dotted var(--color-red-darker)}.vehicle-crew .vehicle-seat.empty{color:var(--color-blue-darker);border:1px dashed var(--color-blue-darker)}.vehicle-crew .vehicle-seat.button{color:var(--color-text-dark-secondary);border:1px dotted var(--color-text-dark-secondary)}.vehicle-crew .vehicle-seat.button:hover,.vehicle-crew .vehicle-seat.button:focus{border-style:solid}.vehicle-crew .vehicle-seat:hover{color:var(--color-green);border-color:var(--color-green)}.vehicle-crew .attribute-bar{position:absolute;left:0;height:6px;width:100%;background-color:#00000080;border:1px solid #000;border-radius:3px}.vehicle-crew .attribute-bar.bar1{bottom:0}.vehicle-crew .attribute-bar.bar2{top:0}.vehicle-crew .attribute-bar-progress{height:100%;border-radius:2px}.blade-runner.sheet.loot{min-width:400px;min-height:600px}.blade-runner.sheet.loot .loot-container{display:grid;gap:4px;grid-template-columns:auto 1fr auto;grid-template-rows:auto auto auto;grid-template-areas:"name name type" "avatar notes type" "loot loot loot"}.blade-runner.sheet.loot .loot-name{grid-area:name}.blade-runner.sheet.loot .loot-name input{border:2px solid var(--color-border);border-radius:0;color:var(--color-blue-darker);font-family:var(--font-table);font-size:var(--font-size--default);text-transform:uppercase;letter-spacing:2px;width:100%;height:auto;padding:4px;margin:0}.blade-runner.sheet.loot .avatar{grid-area:avatar;border:1px solid var(--color-border);border-radius:0;background-color:var(--color-black);padding:0;display:flex;align-items:center;justify-content:center;width:100px;height:100%}.blade-runner.sheet.loot .loot-notes{grid-area:notes}.blade-runner.sheet.loot .loot-type{grid-area:type;color:var(--color-blue);font-family:var(--font-table);font-size:var(--font-size--large);font-weight:700;text-transform:uppercase;writing-mode:vertical-lr;text-align:center}.blade-runner.sheet.loot .loot-inventory{grid-area:loot}.blade-runner.sheet.loot .embedded-items{max-height:100%}.blade-runner.sheet.loot .embedded-item{width:100%}.blade-runner.sheet.loot .embedded-item .photo{width:36px;height:36px;min-width:36px;min-height:36px}.blade-runner.sheet.loot .embedded-item .details{width:100%}.blade-runner.sheet.loot .embedded-item .name{max-width:450px}.application.sheet-config,.application.sheet-config .window-content{overflow:auto}.blade-runner.sheet.item .btn{float:right}.blade-runner.sheet.item label{color:var(--color-green);font-family:var(--font-table);font-size:var(--font-size--very-small);font-weight:700;text-transform:uppercase;text-align:center}.blade-runner.sheet.item .sheet-body{overflow-y:auto;max-height:285px}.blade-runner.sheet.item .photo{border:2px solid var(--color-border);border-radius:0;background-color:var(--color-black);padding:4px;display:flex;align-items:center;justify-content:center;width:200px;height:200px}.blade-runner.sheet.item .item-type{color:var(--color-blue);font-family:var(--font-table);font-size:var(--font-size--large);font-weight:700;text-transform:uppercase;writing-mode:vertical-lr;text-align:center;padding-left:6px}.blade-runner.sheet.item .item-name input{border:2px solid var(--color-border);border-radius:0;color:var(--color-blue-darker);font-family:var(--font-table);font-size:var(--font-size--default);text-transform:uppercase;letter-spacing:2px;height:auto;padding:4px;margin:4px 0 0}.blade-runner.sheet.item .item-property{border:1px solid var(--color-border);border-radius:0;background-color:var(--color-black);padding:4px;display:flex;flex-direction:row;align-items:center;margin-top:4px}.blade-runner.sheet.item .item-property>*{flex:1}.blade-runner.sheet.item .item-attacks,.blade-runner.sheet.item .item-modifiers,.blade-runner.sheet.item .item-active-effects{margin-top:4px}.blade-runner.sheet.item .item-modifiers-list{color:var(--color-blue-lighter);display:grid;grid-template-columns:130px 69px auto;gap:2px}.blade-runner.sheet.item .item-attacks-list{color:var(--color-blue-lighter)}.blade-runner.sheet.item details label{font-size:var(--font-size--extra-small)}.blade-runner.sheet.item summary a{font-size:x-small}.blade-runner.sheet.item .button.delete-action{flex:0;margin-left:2px}.blade-runner.sheet.item .item-action+.item-action{margin-top:4px}.blade-runner.sheet.item .button{font-size:var(--font-size--extra-small);text-align:center;height:fit-content}.blade-runner.sheet.item .empty{font-size:var(--font-size--very-small)}.blade-runner .embedded-items{gap:4px;align-items:flex-start;overflow-y:auto;max-height:200px}.blade-runner .embedded-items .empty-inventory{color:var(--color-blue-lighter);font-size:var(--font-size--very-small)}.blade-runner .embedded-item{background-image:url(./assets/textures/character-sheet-editor.webp);background-size:100% 100%;border:2px solid var(--color-border);border-radius:2px;width:230px;padding:4px;position:relative}.blade-runner .embedded-item .photo{background-color:var(--color-black);border:1px solid var(--color-blue-lighter);border-radius:5px;display:flex;align-items:center;justify-content:center;width:64px;height:64px;min-width:64px;min-height:64px;margin-right:4px}.blade-runner .embedded-item .photo.rollable{border-color:var(--color-yellow)}.blade-runner .embedded-item .details{font-family:var(--font-editor);font-size:var(--font-size--default);color:var(--color-black)}.blade-runner .embedded-item .details .item-detail{line-height:1;margin-bottom:2px}.blade-runner .embedded-item .details .name{color:var(--color-black);font-family:var(--font-table);font-size:var(--font-size--small);font-weight:700;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:146px}.blade-runner .embedded-item .details .name.empty-item{text-decoration:line-through}.blade-runner .embedded-item .details .special{font-weight:700}.blade-runner .embedded-item .details .damage{display:grid;grid-template-columns:repeat(3,auto);column-gap:8px}.blade-runner .embedded-item .details .damage i{font-size:90%}.blade-runner .embedded-item .details .description p,.blade-runner .embedded-item .details .description li{color:var(--color-black)}.blade-runner .embedded-item .details .description p,.blade-runner .embedded-item .details .description ul,.blade-runner .embedded-item .details .description ol{margin:0}.blade-runner .embedded-item.specialty,.blade-runner .embedded-item.upgrade{width:100%}.blade-runner .embedded-item.specialty .photo,.blade-runner .embedded-item.upgrade .photo{width:36px;height:36px;min-width:36px;min-height:36px}.blade-runner .embedded-item.specialty .details,.blade-runner .embedded-item.upgrade .details{width:100%}.blade-runner .embedded-item.specialty .name,.blade-runner .embedded-item.upgrade .name{max-width:450px}.blade-runner .embedded-item .item-controls{color:var(--color-black);background-color:var(--color-blue-lighter);border:1px solid var(--color-blue-darker);border-radius:4px;opacity:0;position:absolute;bottom:4px;right:4px;padding:2px 4px;transition:opacity .25s linear .25s}.blade-runner .embedded-item .item-controls a+a{margin-left:2px}.blade-runner .embedded-item:hover .item-controls{opacity:1}.application.journal-entry{border:1px solid var(--color-blue-darker);border-radius:4px}.application.journal-entry .window-header{border:none}.application.journal-entry .window-header .window-title{font-family:var(--font-header);color:var(--color-blue);text-transform:uppercase;letter-spacing:1px}.application.journal-entry .window-header .header-button{display:flex;flex:none;flex-flow:row-reverse;max-width:24px;height:32px;white-space:nowrap;line-height:2;overflow:hidden;padding:0 4px;margin:0 0 0 2px;color:transparent;transition:max-width 1s ease}.application.journal-entry .window-header .header-button:hover{max-width:200px;color:var(--color-blue)}.application.journal-entry .window-header .header-button>i{font-size:var(--font-size--small);color:var(--color-blue);margin:8px 0 0 8px}.application.journal-entry .journal-sidebar{color:var(--color-blue)}.application.journal-entry .journal-sidebar .directory-header .header-search input{background-image:url(./assets/textures/character-sheet-editor.webp);background-size:100% 100%;color:var(--color-black);font-family:var(--font-table);font-weight:700;text-align:center}.application.journal-entry .journal-sidebar .directory-header .header-search input:disabled,.application.journal-entry .journal-sidebar .directory-header .header-search input.disabled{color:var(--color-disabled)}.application.journal-entry .journal-sidebar .pages-list .directory-item.active:first-of-type,.application.journal-entry .journal-sidebar .pages-list .directory-item.active+:not(.active),.application.journal-entry .journal-sidebar .pages-list .directory-item:not(.active)+.active{border-top-color:var(--color-blue-lighter)}.application.journal-entry .journal-sidebar .pages-list .directory-item.active:last-of-type{border-bottom-color:var(--color-blue-lighter)}.application.journal-entry .directory .action-buttons button{line-height:1}.application.journal-entry .journal-entry-content{color:#fff2de;background-image:url(./assets/textures/journal-sheet-background.webp);background-position:top center;background-size:cover;background-repeat:no-repeat;background-color:var(--color-background)}.application.journal-entry .journal-entry-content .journal-header{margin:4px}.application.journal-entry .journal-entry-content .journal-header .title{font-family:var(--font-headline);font-size:var(--font-size--extra-large);color:var(--color-skin);text-transform:uppercase;word-spacing:.66em;letter-spacing:.5em;border:none;background:transparent}.application.journal-entry .journal-entry-content .journal-page-header{margin-bottom:2em}.application.journal-entry .journal-entry-content h1{color:var(--color-red);font-family:var(--font-headline);font-size:var(--font-size--extra-large);line-height:1.25;word-spacing:.66em;letter-spacing:.5em;text-transform:uppercase;text-align:center;text-shadow:1px 0 0 #200,0 -1px 0 #200,0 1px 0 #200,-1px 0 0 #200}.application.journal-entry .journal-entry-content h2{color:var(--color-red-darker);font-family:var(--font-headline);font-size:var(--font-size--large);line-height:1.2;word-spacing:.18em;letter-spacing:.2em;text-transform:uppercase;padding-bottom:.25em}.application.journal-entry .journal-entry-content h3{color:var(--color-red-darker);font-family:var(--font-headline);font-size:1.3rem;line-height:1.15;letter-spacing:.1em;text-transform:uppercase}.application.journal-entry .journal-entry-content p,.application.journal-entry .journal-entry-content li{font-family:var(--font-text-editor);font-size:var(--font-size--default)}.application.journal-entry .journal-entry-content a[href]{color:var(--color-red-darker)}.application.journal-entry .journal-entry-content code{font-family:inherit;font-weight:400;padding:0 4px;background-color:#999;border-radius:4px;border:none;white-space:pre-wrap}.application.journal-entry .journal-entry-content code img{display:inline}.application.journal-entry .journal-entry-content img{border:none}.application.journal-entry .journal-entry-content figcaption{font-family:var(--font-table);font-weight:400;color:var(--color-skin)}.application.journal-entry .journal-entry-content .notification{display:block}.active-effects-list{color:var(--color-blue-lighter)}.active-effects-list .active-effect{display:flex;flex-direction:row;justify-content:space-between}.active-effects-list .active-effect-name{color:var(--color-green)}.active-effects-list .active-effect-name.suspended{color:var(--color-disabled);text-decoration:line-through}.active-effects-list .active-effect-controls{display:flex;flex-direction:row;gap:4px}.active-effects-list .active-effect-controls .btn:hover{color:var(--color-yellow)}.active-effects-list .active-effect-controls .btn.suspended{color:var(--color-disabled)}.active-effects-list .active-effect-disclaimer{color:var(--color-disabled);font-style:italic;font-weight:400}.blade-runner.roll-application .sheet-body{display:grid;grid-template-columns:1fr 1fr;gap:4px}.blade-runner.roll-application .sheet-footer{margin-top:30px}.blade-runner.roll-application .dice{grid-column:1/-1}.blade-runner.roll-application .dice .die-box{border-radius:10px;font-family:var(--font-table);font-size:var(--font-size--large);color:var(--color-blue-darker);display:flex;align-items:center;justify-content:center;height:100px;width:66.6666666667px}.blade-runner.roll-application .dice .die-box.die-d6{background:center/contain no-repeat url(./assets/icons/d6-grey.svg)}.blade-runner.roll-application .dice .die-box.die-d8{background:center/contain no-repeat url(./assets/icons/d8-grey.svg)}.blade-runner.roll-application .dice .die-box.die-d10{background:center/contain no-repeat url(./assets/icons/d10-grey.svg)}.blade-runner.roll-application .dice .die-box.die-d12{background:center/contain no-repeat url(./assets/icons/d12-grey.svg)}.blade-runner.roll-application .dice .die-box.empty{float:right}.blade-runner.roll-application .dice .die-box+.die-box{margin-left:4px}.blade-runner.roll-application .modifiers{display:grid;grid-template-columns:1fr auto 1fr;grid-template-rows:auto auto 1fr;align-items:center;gap:4px}.blade-runner.roll-application .modifiers h3{grid-column:1/-1}.blade-runner.roll-application .modifiers h4:not(:first-of-type),.blade-runner.roll-application .modifiers .modifier-value{text-align:center}.blade-runner.roll-application .modifiers .modifier-name{color:var(--color-blue-lighter)}.blade-runner.roll-application .modifiers .modifier-value.advantage{color:var(--color-green)}.blade-runner.roll-application .modifiers .modifier-value.disadvantage{color:var(--color-red)}.blade-runner.roll-application .roll-options{gap:4px}.blade-runner.roll-application .roll-options input[type=number]{width:42px}.blade-runner.roll-application .roll-options input[type=checkbox]{margin-left:10px}.blade-runner.roll-application .roll-options .checkbox-label{font-family:var(--font-header);color:var(--color-blue)}.blade-runner.dialog.dice-push-select input:disabled+span{color:var(--color-disabled)}.blade-runner.choice-dialog .dialog-buttons{flex-direction:column;gap:8px;margin:5% 10%}.blade-runner.choice-dialog .dialog-button{line-height:1.2}.blade-runner.choice-dialog .dialog-button .attack-description{font-weight:400;font-size:90%}.blade-runner.choice-dialog .dialog-button .attack-description code{font-weight:700;color:var(--color-blue)}.blade-runner.choice-dialog .dialog-button .attack-description em{color:var(--color-blue)}:root{--font-gridnik: "Foundry Gridnik", "Consolas", sans-serif;--font-urwdin: "URW DIN Condensed", sans-serif}.font-gridnik{font-family:var(--font-gridnik)!important}.font-urwdin{font-family:var(--font-urwdin)!important}.application.journal-entry .journal-entry-content .flexrow.colgap{column-gap:20px}.application.journal-entry .journal-entry-content .flbr-chapter-quote{font-family:var(--font-gridnik);color:var(--color-skin);text-align:center;margin-bottom:20px}.application.journal-entry .journal-entry-content .flbr-chapter-quote.blue{color:var(--color-blue-lighter)}.application.journal-entry .journal-entry-content .flbr-definition{margin:50px}.application.journal-entry .journal-entry-content .flbr-definition p{line-height:1.25;margin:0 0 1.25em;text-shadow:-#000 -#000 0 #000,#000 -#000 0 #000,-#000 #000 0 #000,#000 #000 0 #000}.application.journal-entry .journal-entry-content .flbr-text{color:var(--color-black);margin-bottom:20px;padding:25px;height:fit-content;background-image:url(./assets/textures/page-text-zone-background.webp);background-position:top center;background-size:cover;border:1px solid var(--color-border--core-box-beige)}.application.journal-entry .journal-entry-content .flbr-text p,.application.journal-entry .journal-entry-content .flbr-text li{font-weight:700;line-height:1.2;margin:0}.application.journal-entry .journal-entry-content .flbr-text p+p,.application.journal-entry .journal-entry-content .flbr-text li+p{text-indent:1.5em}.application.journal-entry .journal-entry-content .flbr-text p.no-indent{text-indent:0;margin-top:20px}.application.journal-entry .journal-entry-content .flbr-text.blue{background-image:url(./assets/textures/page-text-zone-background-blue.webp)}.application.journal-entry .journal-entry-content .flbr-text.blue h2,.application.journal-entry .journal-entry-content .flbr-text.blue .flbr-inline-heading{color:var(--color-blue-darker)}.application.journal-entry .journal-entry-content .flbr-text.blue ul li::marker{color:var(--color-blue-darker)}.application.journal-entry .journal-entry-content .flbr-text.blue ol li::marker{color:var(--color-blue-darker)}.application.journal-entry .journal-entry-content .flbr-box{color:var(--color-skin);margin-bottom:20px;padding:25px;height:fit-content;min-width:300px;background-image:url(./assets/textures/journal-sheet-background.webp);background-position:top center;background-size:cover;border:1px solid var(--color-border--core-box-beige)}.application.journal-entry .journal-entry-content .flbr-box h3{color:inherit;font-family:var(--font-gridnik);padding-bottom:.5em}.application.journal-entry .journal-entry-content .flbr-box p,.application.journal-entry .journal-entry-content .flbr-box li{font-family:var(--font-gridnik);font-size:var(--font-size--small);line-height:1.67;margin:0}.application.journal-entry .journal-entry-content .flbr-box p+p,.application.journal-entry .journal-entry-content .flbr-box li+p{text-indent:1.5em}.application.journal-entry .journal-entry-content .flbr-box ul li::marker{color:var(--color-gold)}.application.journal-entry .journal-entry-content .flbr-box.blue{color:var(--color-blue-lighter);border-color:var(--color-border--core-box-blue);background-image:url(./assets/textures/page-text-zone-background-blue-lighter.webp)}.application.journal-entry .journal-entry-content .flbr-box.blue h3{color:var(--color-blue-lighter)}.application.journal-entry .journal-entry-content .flbr-box.blue ul li::marker{color:var(--color-blue-lighter)}.application.journal-entry .journal-entry-content .flbr-box.green{color:var(--color-green);border-color:var(--color-border--core-box-blue);background-image:url(./assets/textures/page-text-zone-background-blue-lighter.webp)}.application.journal-entry .journal-entry-content .flbr-box.green h3{color:var(--color-green)}.application.journal-entry .journal-entry-content .flbr-box.green ul li::marker{color:var(--color-green)}.application.journal-entry .journal-entry-content .flbr-box.yellow-white h3{color:var(--color-yellow)}.application.journal-entry .journal-entry-content .flbr-box.yellow-white p,.application.journal-entry .journal-entry-content .flbr-box.yellow-white li{color:var(--color-white)}.application.journal-entry .journal-entry-content .flbr-box-warning{color:var(--color-red-darker);margin-bottom:20px;padding:25px;height:fit-content;border:1px solid var(--color-red-darker)}.application.journal-entry .journal-entry-content .flbr-box-warning h3,.application.journal-entry .journal-entry-content .flbr-box-warning p{font-family:var(--font-gridnik);font-weight:700}.application.journal-entry .journal-entry-content .flbr-archetype-boxes-grid{display:grid;grid-template-columns:1fr 1fr;gap:20px}.application.journal-entry .journal-entry-content .flbr-archetype-boxes-grid>*{margin:0!important}.application.journal-entry .journal-entry-content .flbr-archetype-skill-box{margin-bottom:20px;padding:25px;border:1px solid var(--color-border--core-box-blue)}.application.journal-entry .journal-entry-content .flbr-archetype-skill-box p{font-family:var(--font-gridnik);color:var(--color-green);padding:1em 4px}.application.journal-entry .journal-entry-content .flbr-archetype-skill-box p:not(:last-of-type){border-bottom:1px solid var(--color-green)}.application.journal-entry .journal-entry-content .flbr-tab-box{display:flex;flex-direction:row;flex-wrap:nowrap}.application.journal-entry .journal-entry-content .flbr-tab-box h3{color:var(--color-white);flex:0;font-family:var(--font-gridnik);writing-mode:vertical-rl;height:fit-content;padding:10px 5px 40px;background-color:#58585840;border:1px solid var(--color-border--core-box-beige);border-bottom-left-radius:99px;text-shadow:-#000 -#000 0 #000,#000 -#000 0 #000,-#000 #000 0 #000,#000 #000 0 #000}.application.journal-entry .journal-entry-content .flbr-tab-box h3 a.content-link{border-bottom:none}.application.journal-entry .journal-entry-content .flbr-tab-box h3 a.content-link i{margin-right:0;margin-bottom:.25em}.application.journal-entry .journal-entry-content .flbr-tab-box div{color:var(--color-white);min-height:200px;min-height:300px;margin-bottom:20px;padding:25px;height:fit-content;min-width:300px;background-image:url(./assets/textures/journal-sheet-background.webp);background-position:top center;background-size:cover;border:1px solid var(--color-border--core-box-beige)}.application.journal-entry .journal-entry-content .flbr-tab-box div h3{color:inherit;font-family:var(--font-gridnik);padding-bottom:.5em}.application.journal-entry .journal-entry-content .flbr-tab-box div p,.application.journal-entry .journal-entry-content .flbr-tab-box div li{font-family:var(--font-gridnik);font-size:var(--font-size--small);line-height:1.67;margin:0}.application.journal-entry .journal-entry-content .flbr-tab-box div p+p,.application.journal-entry .journal-entry-content .flbr-tab-box div li+p{text-indent:1.5em}.application.journal-entry .journal-entry-content .flbr-tab-box div ul li::marker{color:var(--color-gold)}.application.journal-entry .journal-entry-content .flbr-tab-box div.blue{color:var(--color-blue-lighter);border-color:var(--color-border--core-box-blue);background-image:url(./assets/textures/page-text-zone-background-blue-lighter.webp)}.application.journal-entry .journal-entry-content .flbr-tab-box div.blue h3{color:var(--color-blue-lighter)}.application.journal-entry .journal-entry-content .flbr-tab-box div.blue ul li::marker{color:var(--color-blue-lighter)}.application.journal-entry .journal-entry-content .flbr-tab-box div.green{color:var(--color-green);border-color:var(--color-border--core-box-blue);background-image:url(./assets/textures/page-text-zone-background-blue-lighter.webp)}.application.journal-entry .journal-entry-content .flbr-tab-box div.green h3{color:var(--color-green)}.application.journal-entry .journal-entry-content .flbr-tab-box div.green ul li::marker{color:var(--color-green)}.application.journal-entry .journal-entry-content .flbr-tab-box div.yellow-white h3{color:var(--color-yellow)}.application.journal-entry .journal-entry-content .flbr-tab-box div.yellow-white p,.application.journal-entry .journal-entry-content .flbr-tab-box div.yellow-white li{color:var(--color-white)}.application.journal-entry .journal-entry-content .flbr-tab-box.handout h3{color:var(--color-border--core-box-blue);font-size:80%;border-color:var(--color-border--core-box-blue)}.application.journal-entry .journal-entry-content .flbr-tab-box.handout img{border:1px solid var(--color-border--core-box-blue);margin-bottom:20px}.application.journal-entry .journal-entry-content .flbr-sector-box{margin-bottom:20px;padding:25px;border:1px solid var(--color-border--core-box-blue)}.application.journal-entry .journal-entry-content .flbr-sector-box h3,.application.journal-entry .journal-entry-content .flbr-sector-box p,.application.journal-entry .journal-entry-content .flbr-sector-box li{color:var(--color-blue-lighter);font-family:var(--font-gridnik)}.application.journal-entry .journal-entry-content .flbr-sector-box ul li::marker{color:var(--color-blue-lighter)}.application.journal-entry .journal-entry-content .flbr-character-box{margin-bottom:20px;padding:25px;border:1px solid var(--color-green)}.application.journal-entry .journal-entry-content .flbr-character-box img{border:1px solid var(--color-green)}.application.journal-entry .journal-entry-content .flbr-character-box h3{color:var(--color-green)}.application.journal-entry .journal-entry-content .flbr-character-box p{color:var(--color-green);font-family:var(--font-gridnik)}.application.journal-entry .journal-entry-content .flbr-character-box table{font-family:var(--font-gridnik);border-bottom:none;letter-spacing:1px;border:none;margin:0;color:var(--color-green)}.application.journal-entry .journal-entry-content .flbr-character-box table th{font-weight:400;text-align:left}.application.journal-entry .journal-entry-content .flbr-character-box table td{font-size:var(--font-size--small)}.application.journal-entry .journal-entry-content .flbr-character-box table td,.application.journal-entry .journal-entry-content .flbr-character-box table th{padding:7.5px 12.5px}.application.journal-entry .journal-entry-content .flbr-character-box table tr:not(:first-of-type){border-top:1px solid var(--color-green)}.application.journal-entry .journal-entry-content .flbr-character-box table tr:nth-child(even){background-color:initial}.application.journal-entry .journal-entry-content .flbr-character-box table li{font-family:var(--font-gridnik)}.application.journal-entry .journal-entry-content .flbr-item-box{margin-bottom:20px;display:flex;flex-direction:row;flex-wrap:nowrap;border:1px solid var(--color-border--core-box-blue);min-width:300px;background-color:var(--color-background--gray-dust)}.application.journal-entry .journal-entry-content .flbr-item-box .item-image{padding:25px;width:100%;display:flex;flex-direction:column;justify-content:center}.application.journal-entry .journal-entry-content .flbr-item-box .item-features{max-width:40%;padding:12.5px;border-left:1px solid var(--color-border--core-box-blue);border-right:1px solid var(--color-border--core-box-blue);background-image:url(./assets/textures/page-text-zone-background-blue-lighter.webp);background-position:top center;background-size:cover}.application.journal-entry .journal-entry-content .flbr-item-box .item-features table{color:var(--color-gold);text-shadow:-#000 -#000 0 #000,#000 -#000 0 #000,-#000 #000 0 #000,#000 #000 0 #000;font-family:var(--font-gridnik);border-bottom:none;letter-spacing:1px;border:none;margin:0;border-top:none}.application.journal-entry .journal-entry-content .flbr-item-box .item-features table th{font-weight:400;text-align:left}.application.journal-entry .journal-entry-content .flbr-item-box .item-features table td{font-size:var(--font-size--small)}.application.journal-entry .journal-entry-content .flbr-item-box .item-features table td,.application.journal-entry .journal-entry-content .flbr-item-box .item-features table th{padding:7.5px 12.5px}.application.journal-entry .journal-entry-content .flbr-item-box .item-features table tr:not(:first-of-type){border-top:1px solid #000}.application.journal-entry .journal-entry-content .flbr-item-box .item-features table tr:nth-child(even){background-color:initial}.application.journal-entry .journal-entry-content .flbr-item-box .item-features table li{font-family:var(--font-gridnik)}.application.journal-entry .journal-entry-content .flbr-item-box .item-features table td{font-size:var(--font-size--very-small)}.application.journal-entry .journal-entry-content .flbr-item-box .item-features table tr:not(:first-of-type){border-top:3px dotted var(--color-gold)}.application.journal-entry .journal-entry-content .flbr-item-box .item-features table td:first-child .boxed{color:var(--color-gold);font-family:var(--font-gridnik);font-size:var(--font-size--very-small);text-align:center;text-shadow:-#000 -#000 0 #000,#000 -#000 0 #000,-#000 #000 0 #000,#000 #000 0 #000;line-height:2.5;width:2.5em;border:1px solid var(--color-border--core-box-orange);background-color:#ed6c1c4d}.application.journal-entry .journal-entry-content .flbr-item-box img{align-self:center;padding:0 20px}.application.journal-entry .journal-entry-content .flbr-item-box>div{margin-bottom:0;height:auto}.application.journal-entry .journal-entry-content .flbr-table{color:var(--color-skin);margin-bottom:20px;padding:25px;height:fit-content;background-image:url(./assets/textures/journal-sheet-background.webp);background-position:top center;background-size:cover;border:1px solid var(--color-border--core-box-beige)}.application.journal-entry .journal-entry-content .flbr-table h3{color:var(--color-skin);font-family:var(--font-gridnik);padding-left:12.5px}.application.journal-entry .journal-entry-content .flbr-table p{font-family:var(--font-gridnik);font-size:var(--font-size--small);line-height:1.67;padding-left:12.5px;margin-bottom:1.5em}.application.journal-entry .journal-entry-content .flbr-table table{font-family:var(--font-gridnik);border-bottom:none;letter-spacing:1px;border:none;margin:0}.application.journal-entry .journal-entry-content .flbr-table table th{font-weight:400;text-align:left}.application.journal-entry .journal-entry-content .flbr-table table td{font-size:var(--font-size--small)}.application.journal-entry .journal-entry-content .flbr-table table td,.application.journal-entry .journal-entry-content .flbr-table table th{padding:7.5px 12.5px}.application.journal-entry .journal-entry-content .flbr-table table tr:not(:first-of-type){border-top:1px solid var(--color-gold)}.application.journal-entry .journal-entry-content .flbr-table table tr:nth-child(even){background-color:initial}.application.journal-entry .journal-entry-content .flbr-table table li{font-family:var(--font-gridnik)}.application.journal-entry .journal-entry-content .flbr-table.green{border-color:var(--color-border--core-box-blue);background-image:url(./assets/textures/page-text-zone-background-blue-lighter.webp)}.application.journal-entry .journal-entry-content .flbr-table.green h3,.application.journal-entry .journal-entry-content .flbr-table.green p,.application.journal-entry .journal-entry-content .flbr-table.green ul li::marker{color:var(--color-green)}.application.journal-entry .journal-entry-content .flbr-table.green table{color:var(--color-green);border-color:var(--color-green)}.application.journal-entry .journal-entry-content .flbr-table.green table tr{border-color:var(--color-green)}.application.journal-entry .journal-entry-content .flbr-table table+p{font-family:var(--font-gridnik);font-size:var(--font-size--very-small);padding-left:12.5px;padding-top:.5em;border-top:1px solid var(--color-gold)}.application.journal-entry .journal-entry-content .flbr-example{display:flex;flex-direction:row;flex-wrap:nowrap}.application.journal-entry .journal-entry-content .flbr-example h3{color:var(--color-gold);flex:0;font-family:var(--font-gridnik);writing-mode:vertical-rl;height:fit-content;padding:10px 5px 40px;background-color:#58585840;border:1px solid var(--color-border--core-box-beige);border-bottom-left-radius:99px;text-shadow:-#000 -#000 0 #000,#000 -#000 0 #000,-#000 #000 0 #000,#000 #000 0 #000}.application.journal-entry .journal-entry-content .flbr-example div{color:var(--color-gold);min-height:200px;margin-bottom:20px;padding:25px;height:fit-content;min-width:300px;background-image:url(./assets/textures/journal-sheet-background.webp);background-position:top center;background-size:cover;border:1px solid var(--color-border--core-box-beige)}.application.journal-entry .journal-entry-content .flbr-example div h3{color:inherit;font-family:var(--font-gridnik);padding-bottom:.5em}.application.journal-entry .journal-entry-content .flbr-example div p,.application.journal-entry .journal-entry-content .flbr-example div li{font-family:var(--font-gridnik);font-size:var(--font-size--small);line-height:1.67;margin:0}.application.journal-entry .journal-entry-content .flbr-example div p+p,.application.journal-entry .journal-entry-content .flbr-example div li+p{text-indent:1.5em}.application.journal-entry .journal-entry-content .flbr-example div ul li::marker{color:var(--color-gold)}.application.journal-entry .journal-entry-content .flbr-example div.blue{color:var(--color-blue-lighter);border-color:var(--color-border--core-box-blue);background-image:url(./assets/textures/page-text-zone-background-blue-lighter.webp)}.application.journal-entry .journal-entry-content .flbr-example div.blue h3{color:var(--color-blue-lighter)}.application.journal-entry .journal-entry-content .flbr-example div.blue ul li::marker{color:var(--color-blue-lighter)}.application.journal-entry .journal-entry-content .flbr-example div.green{color:var(--color-green);border-color:var(--color-border--core-box-blue);background-image:url(./assets/textures/page-text-zone-background-blue-lighter.webp)}.application.journal-entry .journal-entry-content .flbr-example div.green h3{color:var(--color-green)}.application.journal-entry .journal-entry-content .flbr-example div.green ul li::marker{color:var(--color-green)}.application.journal-entry .journal-entry-content .flbr-example div.yellow-white h3{color:var(--color-yellow)}.application.journal-entry .journal-entry-content .flbr-example div.yellow-white p,.application.journal-entry .journal-entry-content .flbr-example div.yellow-white li{color:var(--color-white)}.application.journal-entry .journal-entry-content .flbr-work-that-crime-scene{margin-bottom:20px;padding:25px;border:1px solid var(--color-border--core-box-blue)}.application.journal-entry .journal-entry-content .flbr-work-that-crime-scene .crime-scene-tip{display:flex;flex-direction:row;padding-bottom:25px}.application.journal-entry .journal-entry-content .flbr-work-that-crime-scene .crime-scene-tip:not(:first-child){border-top:2px dotted var(--color-green)}.application.journal-entry .journal-entry-content .flbr-work-that-crime-scene .crime-scene-icon{display:flex;align-items:center;justify-content:center;margin-right:12.5px;margin-top:12.5px;padding:4px;min-width:50px;width:50px;height:50px;border:2px double var(--color-border--core-box-orange)}.application.journal-entry .journal-entry-content .flbr-work-that-crime-scene .crime-scene-icon img{max-width:100%}.application.journal-entry .journal-entry-content .flbr-work-that-crime-scene p{font-family:var(--font-gridnik);font-size:var(--font-size--small);color:var(--color-green);margin-top:12.5px}.application.journal-entry .journal-entry-content .flbr-inline-heading{font-family:var(--font-gridnik);font-weight:700;color:var(--color-red-darker);text-transform:uppercase}.application.journal-entry .journal-entry-content .color-white{color:var(--color-white)!important}.application.journal-entry .journal-entry-content .color-red{color:var(--color-red-darker)!important}.application.journal-entry .journal-entry-content .color-green{color:var(--color-green)!important}.application.journal-entry .journal-entry-content .color-blue{color:var(--color-blue-lighter)!important}.application.journal-entry .journal-entry-content .color-blue-darker{color:var(--color-blue-darker)!important}.application.journal-entry .journal-entry-content .color-skin{color:var(--color-skin)!important}.application.journal-entry .journal-entry-content .color-yellow{color:var(--color-gold)!important}.application.journal-entry .journal-entry-content .smallcaps{font-variant:small-caps;font-style:normal;font-weight:400;font-size:.95em}.application.journal-entry .journal-entry-content .small{font-size:var(--font-size--extra-small)}.application.journal-entry .journal-entry-content ul li{padding-left:1em}.application.journal-entry .journal-entry-content ul li::marker{color:var(--color-red-darker);font-family:Square Things;font-size:var(--font-size--extra-small);font-weight:400;content:"b"}.application.journal-entry .journal-entry-content ul li.full-square-marker::marker{content:"B"}.application.journal-entry .journal-entry-content ul li ul{padding-left:1em}.application.journal-entry .journal-entry-content ul li ul li::marker{content:"a"}.application.journal-entry .journal-entry-content ul li ul ul li::marker{content:"A"}.application.journal-entry .journal-entry-content ol li{padding-left:1em}.application.journal-entry .journal-entry-content ol li::marker{color:var(--color-red-darker);font-weight:700}.application.journal-entry .journal-entry-content .flbr-boxed-number{flex:0;height:fit-content;margin-right:10px;background-color:var(--color-blue-lighter)}.application.journal-entry .journal-entry-content .flbr-boxed-number p{font-family:var(--font-gridnik);font-size:var(--font-size--large);font-weight:700;color:var(--color-black);margin:.4em .6em;text-shadow:-#000 -#000 0 #000,#000 -#000 0 #000,-#000 #000 0 #000,#000 #000 0 #000}.application.journal-entry .journal-entry-content .flbr-boxed-number.red{background-color:var(--color-red-darker)}.application.journal-entry .journal-entry-content .flbr-boxed-number.red p{color:var(--color-skin)}.application.journal-entry .journal-entry-content .flbr-story{margin:1.2em 0;font-style:italic}.application.journal-entry .journal-entry-content .flbr-story p:first-of-type{text-indent:0}.application.journal-entry .journal-entry-content table.countdown-events tbody{vertical-align:top}.application.journal-entry .journal-entry-content table.countdown-events tr td:not(:first-child),.application.journal-entry .journal-entry-content table.countdown-events tr th:not(:first-child){text-align:center} diff --git a/blade-runner.js b/blade-runner.js new file mode 100644 index 0000000..d466803 --- /dev/null +++ b/blade-runner.js @@ -0,0 +1,63 @@ +var __defProp=Object.defineProperty;var __defNormalProp=(obj,key,value)=>key in obj?__defProp(obj,key,{enumerable:!0,configurable:!0,writable:!0,value}):obj[key]=value;var __publicField=(obj,key,value)=>(__defNormalProp(obj,typeof key!="symbol"?key+"":key,value),value);var define_TEMPLATE_PATHS_default=["systems/blade-runner/templates/actor/actor-crit-chatcard.hbs","systems/blade-runner/templates/actor/actor-damage-chatcard.hbs","systems/blade-runner/templates/actor/actor-sheet-config.hbs","systems/blade-runner/templates/actor/character/character-limited-sheet.hbs","systems/blade-runner/templates/actor/character/character-sheet.hbs","systems/blade-runner/templates/actor/character/sheet-tabs/bio-tab.hbs","systems/blade-runner/templates/actor/character/sheet-tabs/combat-tab.hbs","systems/blade-runner/templates/actor/character/sheet-tabs/inventory-tab.hbs","systems/blade-runner/templates/actor/character/sheet-tabs/mods-tab.hbs","systems/blade-runner/templates/actor/character/sheet-tabs/stats-tab.hbs","systems/blade-runner/templates/actor/loot/loot-sheet.hbs","systems/blade-runner/templates/actor/inventory-partial.hbs","systems/blade-runner/templates/actor/vehicle/vehicle-seat-partial.hbs","systems/blade-runner/templates/actor/vehicle/vehicle-sheet.hbs","systems/blade-runner/templates/components/dialog/range-picker-dialog.hbs","systems/blade-runner/templates/components/dialog/table-draw-dialog.hbs","systems/blade-runner/templates/components/roll/roll-askdie-dialog.hbs","systems/blade-runner/templates/components/roll/roll-chatcard-infos-partial.hbs","systems/blade-runner/templates/components/roll/roll-chatcard-tooltip-partial.hbs","systems/blade-runner/templates/components/roll/roll-chatcard.hbs","systems/blade-runner/templates/components/roll/roll-push-select-dialog.hbs","systems/blade-runner/templates/components/roll/roller.hbs","systems/blade-runner/templates/item/actions-partial.hbs","systems/blade-runner/templates/item/active-effects-partial.hbs","systems/blade-runner/templates/item/attacks-partial.hbs","systems/blade-runner/templates/item/item-chatcard.hbs","systems/blade-runner/templates/item/item-sheet.hbs","systems/blade-runner/templates/item/modifiers-partial.hbs"];var SYSTEM_ID="blade-runner",DIE_SCORES={A:12,B:10,C:8,D:6},ATTRIBUTES={STRENGTH:"str",AGILITY:"agi",INTELLIGENCE:"int",EMPATHY:"emp",VEHICLE_MANEUVERABILITY:"mvr"},SKILLS={FORCE:"force",CLOSE_COMBAT:"closeCombat",STAMINA:"stamina",FIREARMS:"firearms",MOBILITY:"mobility",STEALTH:"stealth",MEDICAL_AID:"medicalAid",OBSERVATION:"observation",TECH:"tech",DRIVING:"driving",CONNECTIONS:"connections",INSIGHT:"insight",MANIPULATION:"manipulation"},CAPACITIES={HEALTH:"health",RESOLVE:"resolve"},NATURES={HUMAN:"human",REPLICANT:"replicant"},ARCHETYPES={ANALYST:"analyst",CITY_SPEAKER:"citySpeaker",DOXIE:"doxie",ENFORCER:"enforcer",FIXER:"fixer",INSPECTOR:"inspector",SKIMMER:"skimmer"},RANGES={ENGAGED:0,SHORT:1,MEDIUM:2,LONG:3,EXTREME:4},COMBAT_ACTIONS={SPRINT:"action-sprint",CRAWL:"action-crawl",UNARMED_ATTACK:"action-unarmed-attack",MELEE_ATTACK:"action-melee-attack",GRAPPLE:"action-grapple",BREAK_FREE:"action-break-free",SHOOT_FIREARM:"action-shoot-firearm",CAREFUL_AIM:"action-careful-aim",THROW_WEAPON:"action-throw-weapon",FIRST_AID:"action-first-aid",MANIPULATE:"action-manipulate",USE_ITEM:"action-use-item",VEHICLE_CRASH:"action-vehicle-crash",VEHICLE_MASSIVE_CRASH:"action-vehicle-massive-crash",VEHICLE_RAMMING:"action-vehicle-ramming",VEHICLE_REPAIR:"action-vehicle-repair",VEHICLE_SPEEDING:"action-vehicle-speeding",VEHICLE_EXPLODE:"action-vehicle-explode"},DAMAGE_TYPES={NONE:0,CRUSHING:1,PIERCING:2,STRESS:3},AVAILABILITIES={INCIDENTAL:5,STANDARD:4,PREMIUM:3,RARE:2,LUXURY:1},ACTOR_TYPES={CHAR:"character",VEHICLE:"vehicle",LOOT:"loot"},ACTOR_SUBTYPES={PC:"pc",NPC:"npc"},ITEM_TYPES={GENERIC:"generic",SYNTHETIC_AUGMENTATION:"upgrade",ARMOR:"armor",WEAPON:"weapon",EXPLOSIVE:"explosive",SPECIALTY:"specialty",CRITICAL_INJURY:"injury"},SETTINGS_KEYS={SYSTEM_MIGRATION_VERSION:"systemMigrationVersion",DISPLAYED_MESSAGES:"messages",AUTO_APPLY_DAMAGE:"autoApplyDamage",AUTO_ARMOR_ROLL:"autoArmorRoll",CRASH_TABLE:"crashTable",CRUSHING_TABLE:"crushingTable",DO_NOT_USE_HANDWRITTEN_FONT:"doNotUseHandwrittenFont",EDIT_NATURE_PERMISSION:"editNaturePermission",OPEN_FIRST_WEAPON_ATTACK:"openFirstWeaponAttack",PIERCING_TABLE:"piercingTable",UPDATE_ACTOR_MANEUVERABILITY_ON_CREW:"updateActorMvrOnCrew",UPDATE_ACTOR_MANEUVERABILITY_ON_UNCREW:"updateActorMvrOnUncrew",USE_ACTIVE_EFFECTS:"useActiveEffects"};var FLBR={};FLBR.systemMacroFolder="Blade Runner Macros";FLBR.scoreMap=new Map;FLBR.scoreMap.set("\u2013",0);for(let[k,v]of Object.entries(DIE_SCORES))FLBR.scoreMap.set(k,v);FLBR.dieMap=new Map(Array.from(FLBR.scoreMap,([n,v])=>[v,n]));FLBR.vehicleSkill=SKILLS.DRIVING;FLBR.vehicleAttribute=ATTRIBUTES.VEHICLE_MANEUVERABILITY;FLBR.attributes=Object.values(ATTRIBUTES).filter(a=>a!==FLBR.vehicleAttribute);FLBR.skillMap={[SKILLS.CLOSE_COMBAT]:ATTRIBUTES.STRENGTH,[SKILLS.FORCE]:ATTRIBUTES.STRENGTH,[SKILLS.STAMINA]:ATTRIBUTES.STRENGTH,[SKILLS.FIREARMS]:ATTRIBUTES.AGILITY,[SKILLS.MOBILITY]:ATTRIBUTES.AGILITY,[SKILLS.STEALTH]:ATTRIBUTES.AGILITY,[SKILLS.MEDICAL_AID]:ATTRIBUTES.INTELLIGENCE,[SKILLS.OBSERVATION]:ATTRIBUTES.INTELLIGENCE,[SKILLS.TECH]:ATTRIBUTES.INTELLIGENCE,[SKILLS.CONNECTIONS]:ATTRIBUTES.EMPATHY,[SKILLS.INSIGHT]:ATTRIBUTES.EMPATHY,[SKILLS.MANIPULATION]:ATTRIBUTES.EMPATHY,[SKILLS.DRIVING]:ATTRIBUTES.VEHICLE_MANEUVERABILITY};FLBR.skills=Object.keys(FLBR.skillMap);FLBR.archetypes={};for(let[k,v]of Object.entries(ARCHETYPES))FLBR.archetypes[v]=`FLBR.ARCHETYPE.${k}`;FLBR.capacitiesMap={[CAPACITIES.HEALTH]:{attributes:[ATTRIBUTES.STRENGTH,ATTRIBUTES.AGILITY],max:10},[CAPACITIES.RESOLVE]:{attributes:[ATTRIBUTES.INTELLIGENCE,ATTRIBUTES.EMPATHY],max:10}};FLBR.natures={[NATURES.HUMAN]:"FLBR.NATURE.Human",[NATURES.REPLICANT]:"FLBR.NATURE.Replicant"};FLBR.natureModifierMap={[NATURES.HUMAN]:{[CAPACITIES.HEALTH]:0,[CAPACITIES.RESOLVE]:0},[NATURES.REPLICANT]:{[CAPACITIES.HEALTH]:2,[CAPACITIES.RESOLVE]:-2}};FLBR.maxPushMap={[NATURES.HUMAN]:1,[NATURES.REPLICANT]:2};FLBR.pushTraumaMap={[NATURES.HUMAN]:{[ATTRIBUTES.STRENGTH]:CAPACITIES.HEALTH,[ATTRIBUTES.AGILITY]:CAPACITIES.HEALTH,[ATTRIBUTES.INTELLIGENCE]:CAPACITIES.RESOLVE,[ATTRIBUTES.EMPATHY]:CAPACITIES.RESOLVE},[NATURES.REPLICANT]:{[ATTRIBUTES.STRENGTH]:CAPACITIES.RESOLVE,[ATTRIBUTES.AGILITY]:CAPACITIES.RESOLVE,[ATTRIBUTES.INTELLIGENCE]:CAPACITIES.RESOLVE,[ATTRIBUTES.EMPATHY]:CAPACITIES.RESOLVE}};FLBR.characterSubtypes={[ACTOR_SUBTYPES.PC]:"ACTOR.SubtypePc",[ACTOR_SUBTYPES.NPC]:"ACTOR.SubtypeNpc"};FLBR.physicalItems=[ITEM_TYPES.GENERIC,ITEM_TYPES.WEAPON,ITEM_TYPES.ARMOR,ITEM_TYPES.EXPLOSIVE];FLBR.allowedItems={[ACTOR_TYPES.CHAR]:[ITEM_TYPES.SPECIALTY,ITEM_TYPES.SYNTHETIC_AUGMENTATION,ITEM_TYPES.CRITICAL_INJURY],[ACTOR_TYPES.VEHICLE]:[],[ACTOR_TYPES.LOOT]:[]};FLBR.ranges={[RANGES.ENGAGED]:"FLBR.WEAPON_RANGE.Engaged",[RANGES.SHORT]:"FLBR.WEAPON_RANGE.Short",[RANGES.MEDIUM]:"FLBR.WEAPON_RANGE.Medium",[RANGES.LONG]:"FLBR.WEAPON_RANGE.Long",[RANGES.EXTREME]:"FLBR.WEAPON_RANGE.Extreme"};FLBR.availabilities={[AVAILABILITIES.INCIDENTAL]:"FLBR.ITEM_AVAILABILITY.Incidental",[AVAILABILITIES.STANDARD]:"FLBR.ITEM_AVAILABILITY.Standard",[AVAILABILITIES.PREMIUM]:"FLBR.ITEM_AVAILABILITY.Premium",[AVAILABILITIES.RARE]:"FLBR.ITEM_AVAILABILITY.Rare",[AVAILABILITIES.LUXURY]:"FLBR.ITEM_AVAILABILITY.Luxury"};FLBR.damageTypes={[DAMAGE_TYPES.NONE]:"FLBR.WEAPON_DAMAGE_TYPE.None",[DAMAGE_TYPES.CRUSHING]:"FLBR.WEAPON_DAMAGE_TYPE.Crushing",[DAMAGE_TYPES.PIERCING]:"FLBR.WEAPON_DAMAGE_TYPE.Piercing",[DAMAGE_TYPES.STRESS]:"FLBR.WEAPON_DAMAGE_TYPE.Stress"};FLBR.blastPowerMap={12:{damage:4,crit:12},10:{damage:3,crit:10},8:{damage:2,crit:8},6:{damage:1,crit:6}};FLBR.startingAttributeLevel=8;FLBR.startingSkillLevel=6;FLBR.deathSaveTest=SKILLS.STAMINA;FLBR.baselineTest=SKILLS.INSIGHT;FLBR.maxPromotionPoints=20;FLBR.maxHumanityPoints=20;FLBR.maxChinyenPoints=20;FLBR.maxVehicleHull=10;FLBR.maxRolledDice=3;FLBR.itemSpecialInputMaxLength=80;FLBR.vehicleCrashDamage="1d3 + @altitude";FLBR.vehicleMassiveCrashDamage="1d6 + @altitude";FLBR.vehicleExplosionBlastPower=10;FLBR.rollModes={};for(let[k,v]of Object.entries(CONST.DICE_ROLL_MODES))FLBR.rollModes[v]=`CHAT.Roll${k.toLowerCase().capitalize()}`;FLBR.Actions=[{id:COMBAT_ACTIONS.SPRINT,label:"FLBR.COMBAT_ACTION.Sprint",hint:"FLBR.COMBAT_ACTION_HINT.Sprint",skill:SKILLS.MOBILITY,attribute:null,callback:null,actorType:ACTOR_TYPES.CHAR},{id:COMBAT_ACTIONS.CRAWL,label:"FLBR.COMBAT_ACTION.Crawl",hint:"FLBR.COMBAT_ACTION_HINT.Crawl",skill:null,callback:()=>ui.notifications.warn("Not implemented yet."),actorType:ACTOR_TYPES.CHAR},{id:COMBAT_ACTIONS.UNARMED_ATTACK,label:"FLBR.COMBAT_ACTION.UnarmedAttack",hint:"FLBR.COMBAT_ACTION_HINT.UnarmedAttack",skill:SKILLS.CLOSE_COMBAT,actorType:ACTOR_TYPES.CHAR},{id:COMBAT_ACTIONS.MELEE_ATTACK,label:"FLBR.COMBAT_ACTION.MeleeAttack",hint:"FLBR.COMBAT_ACTION_HINT.MeleeAttack",skill:SKILLS.CLOSE_COMBAT,actorType:ACTOR_TYPES.CHAR},{id:COMBAT_ACTIONS.GRAPPLE,label:"FLBR.COMBAT_ACTION.Grapple",hint:"FLBR.COMBAT_ACTION_HINT.Grapple",skill:SKILLS.CLOSE_COMBAT,actorType:ACTOR_TYPES.CHAR},{id:COMBAT_ACTIONS.BREAK_FREE,label:"FLBR.COMBAT_ACTION.BreakFree",hint:"FLBR.COMBAT_ACTION_HINT.BreakFree",skill:SKILLS.CLOSE_COMBAT,actorType:ACTOR_TYPES.CHAR},{id:COMBAT_ACTIONS.SHOOT_FIREARM,label:"FLBR.COMBAT_ACTION.ShootFirearm",hint:"FLBR.COMBAT_ACTION_HINT.ShootFirearm",skill:SKILLS.FIREARMS,actorType:ACTOR_TYPES.CHAR},{id:COMBAT_ACTIONS.CAREFUL_AIM,label:"FLBR.COMBAT_ACTION.CarefulAim",hint:"FLBR.COMBAT_ACTION_HINT.CarefulAim",callback:()=>ui.notifications.warn("Not implemented yet."),actorType:ACTOR_TYPES.CHAR},{id:COMBAT_ACTIONS.THROW_WEAPON,label:"FLBR.COMBAT_ACTION.ThrowWeapon",hint:"FLBR.COMBAT_ACTION_HINT.ThrowWeapon",skill:SKILLS.FIREARMS,actorType:ACTOR_TYPES.CHAR},{id:COMBAT_ACTIONS.FIRST_AID,label:"FLBR.COMBAT_ACTION.FirstAid",hint:"FLBR.COMBAT_ACTION_HINT.FirstAid",skill:SKILLS.MEDICAL_AID,actorType:ACTOR_TYPES.CHAR},{id:COMBAT_ACTIONS.MANIPULATE,label:"FLBR.COMBAT_ACTION.Manipulate",hint:"FLBR.COMBAT_ACTION_HINT.Manipulate",skill:SKILLS.MANIPULATION,actorType:ACTOR_TYPES.CHAR},{id:COMBAT_ACTIONS.USE_ITEM,label:"FLBR.COMBAT_ACTION.UseItem",hint:"FLBR.COMBAT_ACTION_HINT.UseItem",callback:()=>ui.notifications.warn("Not implemented yet."),actorType:ACTOR_TYPES.CHAR},{id:COMBAT_ACTIONS.VEHICLE_SPEEDING,label:"FLBR.VEHICLE.Action.Speeding",hint:"FLBR.VEHICLE.Action.SpeedingHint",skill:SKILLS.MOBILITY,actorType:ACTOR_TYPES.VEHICLE,onCrew:!0},{id:COMBAT_ACTIONS.VEHICLE_RAMMING,label:"FLBR.VEHICLE.Action.Ramming",hint:"FLBR.VEHICLE.Action.RammingHint",actorType:ACTOR_TYPES.VEHICLE,callback:actor=>actor.rollRamming()},{id:COMBAT_ACTIONS.VEHICLE_REPAIR,label:"FLBR.VEHICLE.Action.Repair",hint:"FLBR.VEHICLE.Action.RepairHint",skill:SKILLS.TECH,actorType:ACTOR_TYPES.VEHICLE,onCrew:!0},{id:COMBAT_ACTIONS.VEHICLE_CRASH,label:"FLBR.VEHICLE.Action.Crash",hint:"FLBR.VEHICLE.Action.CrashHint",actorType:ACTOR_TYPES.VEHICLE,callback:actor=>actor.crashVehicle()},{id:COMBAT_ACTIONS.VEHICLE_MASSIVE_CRASH,label:"FLBR.VEHICLE.Action.MassiveCrash",hint:"FLBR.VEHICLE.Action.CrashHint",actorType:ACTOR_TYPES.VEHICLE,callback:actor=>actor.crashVehicle(!0)},{id:COMBAT_ACTIONS.VEHICLE_EXPLODE,label:"FLBR.VEHICLE.Action.Explode",hint:"FLBR.VEHICLE.Action.ExplodeHint",actorType:ACTOR_TYPES.VEHICLE,callback:actor=>actor.explodeVehicle()}];FLBR.Icons={tabs:{action:'',bio:'',combat:'',features:'',inventory:'',mods:'',roll:'',stats:''},boxes:{empty:'',full:'',lost:''},dice:{success:'',failure:''},links:{choice:'',rolltable:'',drawtable:''},buttons:{action:'',edit:'',delete:'',remove:'',plus:'',minus:'',advantage:'',disadvantage:'',mount:'',unmount:'',attack:'',armor:'',bomb:'',melee:'',gun:'',crit:'',crush:'',pierce:'',stress:'',range:'',damage:'',lethal:'',vehicle:'',wheel:'',seat:'',chat:'',roll:'',onoff:''}};FLBR.customItemTypesTemplates={};var ActorAction=class{constructor(actionData){if(!actionData.id)throw new SyntaxError("Missing ID for actor action");this.id=actionData.id,this.actorType=actionData.actorType,this._label=actionData.label,this._hint=actionData.hint,this.attribute=actionData.attribute,this.skill=actionData.skill,this.onCrew=actionData.onCrew,this.callback=actionData.callback}get label(){return game.i18n.localize(this._label)}get hint(){return game.i18n.localize(this._hint)}async execute(actor){if(typeof this.callback=="function")return this.callback(actor);let attribute=this.attribute||FLBR.skillMap[this.skill],title=`${actor.name}: ${this.label} (${game.i18n.localize(`FLBR.SKILL.${this.skill.capitalize()}`)})`;return actor.rollStat(attribute,this.skill,{title})}toObject(){return{id:this.id,actorType:this.actorType,label:this._label,hint:this._hint,attribute:this.attribute,skill:this.skill,onCrew:this.onCrew,callback:this.callback}}},ActionCollection=class extends foundry.utils.Collection{constructor(entries){super(entries.map(([id,data])=>data instanceof ActorAction?[id,data]:[id,new ActorAction(data)]))}add(actionData){return actionData instanceof ActorAction?this.set(actionData.id,actionData):this.set(actionData.id,new ActorAction(actionData))}};var YearZeroDie=class extends foundry.dice.terms.Die{constructor(termData={}){termData.faces=Number.isInteger(termData.faces)?termData.faces:6,super(termData),this.maxPush==null&&(this.maxPush=termData.maxPush??1)}get denomination(){return this.constructor.DENOMINATION}get type(){return this.constructor.TYPE}get pushable(){if(this.pushCount>=this.maxPush)return!1;for(let r of this.results)if(!(!r.active||r.discarded)&&!this.constructor.LOCKED_VALUES.includes(r.result))return!0;return!1}get pushCount(){return this.results.reduce((c,r)=>Math.max(c,r.indexPush||0),0)}get pushed(){return this.pushCount>0}get isYearZeroDie(){return!0}get success(){if(!this._evaluated)return;let s=this.results.reduce((tot,r)=>r.active?r.count!==void 0?tot+r.count:this.constructor.SUCCESS_TABLE?tot+this.constructor.SUCCESS_TABLE[r.result]:tot+(r.result>=6?1:0):tot,0);return this.type==="neg"?-s:s}get failure(){if(this._evaluated)return this.results.reduce((tot,r)=>r.active?tot+(r.result<=1):tot,0)}async roll(options={}){let roll=await super.roll(options);return roll.indexResult=options.indexResult,roll.indexResult==null&&(roll.indexResult=1+this.results.reduce((c,r)=>{let i=r.indexResult;return i==null&&(i=-1),Math.max(c,i)},-1)),roll.indexPush=options.indexPush??this.pushCount,this.results[this.results.length-1]=roll,roll}count(n){return this.values.filter(v=>v===n).length}push(){if(!this.pushable)return this;let indexPush=this.pushCount+1,indexesResult=[];for(let r of this.results)!r.active||r.locked||(this.constructor.LOCKED_VALUES.includes(r.result)?r.hidden=!0:(r.active=!1,r.discarded=!0,r.pushed=!0,r.hidden=!0,indexesResult.push(r.indexResult)));for(let i=0;i=6,isMin=result.result===1&&!noMin.includes(this.type)}else isMax=result.result===this.faces,isMin=result.result===1;return[this.constructor.name.toLowerCase(),"d"+this.faces,hasSuccess?"success":null,hasFailure?"failure":null,result.rerolled?"rerolled":null,result.exploded?"exploded":null,result.discarded?"discarded":null,result.pushed?"pushed":null,!(hasSuccess||hasFailure)&&isMin?"min":null,!(hasSuccess||hasFailure)&&isMax?"max":null]}getTooltipData(){return{formula:this.expression,total:this.success,banes:this.failure,faces:this.faces,number:this.number,type:this.type,isYearZeroDie:this.isYearZeroDie,flavor:this.options.flavor??(CONFIG.YZUR?.Dice?.localizeDieTerms?game.i18n.localize(`YZUR.DIETERMS.${this.constructor.name}`):null),rolls:this.results.map(r=>({result:this.getResultLabel(r),classes:this.getResultCSS(r).filterJoin(" "),row:r.indexPush,col:r.indexResult}))}}};YearZeroDie.TYPE="blank";YearZeroDie.LOCKED_VALUES=[6];YearZeroDie.SERIALIZE_ATTRIBUTES.push("maxPush");YearZeroDie.MODIFIERS=foundry.utils.mergeObject({p:"setpush",np:"nopush"},foundry.dice.terms.Die.MODIFIERS);var BaseDie=class extends YearZeroDie{};BaseDie.TYPE="base";BaseDie.DENOMINATION="b";BaseDie.LOCKED_VALUES=[1,6];var SkillDie=class extends YearZeroDie{};SkillDie.TYPE="skill";SkillDie.DENOMINATION="s";var GearDie=class extends YearZeroDie{};GearDie.TYPE="gear";GearDie.DENOMINATION="g";GearDie.LOCKED_VALUES=[1,6];var NegativeDie=class extends SkillDie{};NegativeDie.TYPE="neg";NegativeDie.DENOMINATION="n";var StressDie=class extends YearZeroDie{};StressDie.TYPE="stress";StressDie.DENOMINATION="z";StressDie.LOCKED_VALUES=[1,6];var ArtifactDie=class extends SkillDie{getResultLabel(result){return CONFIG.YZUR.Icons.getLabel(`d${this.constructor.DENOMINATION}`,result.result)}};ArtifactDie.TYPE="arto";ArtifactDie.SUCCESS_TABLE=[null,0,0,0,0,0,1,1,2,2,3,3,4];ArtifactDie.LOCKED_VALUES=[6,7,8,9,10,11,12];var D8ArtifactDie=class extends ArtifactDie{constructor(termData={}){termData.faces=8,super(termData)}};D8ArtifactDie.DENOMINATION="8";var D10ArtifactDie=class extends ArtifactDie{constructor(termData={}){termData.faces=10,super(termData)}};D10ArtifactDie.DENOMINATION="10";var D12ArtifactDie=class extends ArtifactDie{constructor(termData={}){termData.faces=12,super(termData)}};D12ArtifactDie.DENOMINATION="12";var TwilightDie=class extends ArtifactDie{getResultLabel(result){return CONFIG.YZUR.Icons.getLabel("base",result.result)}};TwilightDie.TYPE="base";TwilightDie.SUCCESS_TABLE=[null,0,0,0,0,0,1,1,1,1,2,2,2];TwilightDie.LOCKED_VALUES=[1,6,7,8,9,10,11,12];var D6TwilightDie=class extends TwilightDie{constructor(termData={}){termData.faces=6,super(termData)}};D6TwilightDie.DENOMINATION="6";var D8TwilightDie=class extends TwilightDie{constructor(termData={}){termData.faces=8,super(termData)}};D8TwilightDie.DENOMINATION="8";var D10TwilightDie=class extends TwilightDie{constructor(termData={}){termData.faces=10,super(termData)}};D10TwilightDie.DENOMINATION="10";var D12TwilightDie=class extends TwilightDie{constructor(termData={}){termData.faces=12,super(termData)}};D12TwilightDie.DENOMINATION="12";var AmmoDie=class extends YearZeroDie{constructor(termData={}){termData.faces=6,super(termData)}};AmmoDie.TYPE="ammo";AmmoDie.DENOMINATION="m";AmmoDie.LOCKED_VALUES=[1,6];var LocationDie=class extends YearZeroDie{constructor(termData={}){termData.faces=6,super(termData)}get pushable(){return!1}roll(options){let roll=super.roll(options);return roll.count=0,this.results[this.results.length-1]=roll,roll}};LocationDie.TYPE="loc";LocationDie.DENOMINATION="l";LocationDie.SUCCESS_TABLE=[null,0,0,0,0,0,0];LocationDie.LOCKED_VALUES=[1,2,3,4,5,6];var BladeRunnerDie=class extends ArtifactDie{getResultLabel(result){return CONFIG.YZUR.Icons.getLabel("base",result.result)}};BladeRunnerDie.TYPE="base";BladeRunnerDie.SUCCESS_TABLE=[null,0,0,0,0,0,1,1,1,1,2,2,2];BladeRunnerDie.LOCKED_VALUES=[1];var D6BladeRunnerDie=class extends BladeRunnerDie{constructor(termData={}){termData.faces=6,super(termData)}};D6BladeRunnerDie.DENOMINATION="6";D6BladeRunnerDie.LOCKED_VALUES=[1,6];var D8BladeRunnerDie=class extends BladeRunnerDie{constructor(termData={}){termData.faces=8,super(termData)}};D8BladeRunnerDie.DENOMINATION="8";D8BladeRunnerDie.LOCKED_VALUES=[1,6,7,8];var D10BladeRunnerDie=class extends BladeRunnerDie{constructor(termData={}){termData.faces=10,super(termData)}};D10BladeRunnerDie.DENOMINATION="10";D10BladeRunnerDie.LOCKED_VALUES=[1,10];var D12BladeRunnerDie=class extends BladeRunnerDie{constructor(termData={}){termData.faces=12,super(termData)}};D12BladeRunnerDie.DENOMINATION="12";D12BladeRunnerDie.LOCKED_VALUES=[1,10,11,12];var YZUR={game:"",Chat:{showInfos:!0,diceSorting:["base","skill","neg","gear","arto","loc","ammo"]},Roll:{chatTemplate:"templates/dice/roll.html",tooltipTemplate:"templates/dice/tooltip.html",infosTemplate:"templates/dice/infos.hbs"},Dice:{localizeDieTerms:!0,DIE_TYPES:["base","skill","neg","gear","stress","arto","ammo","loc"],DIE_TERMS:{base:BaseDie,skill:SkillDie,neg:NegativeDie,gear:GearDie,stress:StressDie,artoD8:D8ArtifactDie,artoD10:D10ArtifactDie,artoD12:D12ArtifactDie,a:D12TwilightDie,b:D10TwilightDie,c:D8TwilightDie,d:D6TwilightDie,ammo:AmmoDie,loc:LocationDie,brD12:D12BladeRunnerDie,brD10:D10BladeRunnerDie,brD8:D8BladeRunnerDie,brD6:D6BladeRunnerDie}},Icons:{getLabel:function(type,result){return["d8","d10","d12"].includes(type)&&(type="arto"),String(CONFIG.YZUR.Icons[CONFIG.YZUR.game][type][result])},myz:{base:{1:"\u2623",2:2,3:3,4:4,5:5,6:"\u2622"},skill:{1:1,2:2,3:3,4:4,5:5,6:"\u2622"},neg:{1:1,2:2,3:3,4:4,5:5,6:"\u2796"},gear:{1:"\u{1F4A5}",2:2,3:3,4:4,5:5,6:"\u2622"}},fbl:{base:{1:"\u2620",2:2,3:3,4:4,5:5,6:"\u2694\uFE0F"},skill:{1:1,2:2,3:3,4:4,5:5,6:"\u2694\uFE0F"},neg:{1:1,2:2,3:3,4:4,5:5,6:"\u2796"},gear:{1:"\u{1F4A5}",2:2,3:3,4:4,5:5,6:"\u2694\uFE0F"},arto:{1:1,2:2,3:3,4:4,5:5,6:6,7:7,8:8,9:9,10:10,11:11,12:12}},alien:{skill:{1:1,2:2,3:3,4:4,5:5,6:"\u{1F4A0}"},stress:{1:"\u{1F631}",2:2,3:3,4:4,5:5,6:"\u{1F4A0}"}},tales:{skill:{1:1,2:2,3:3,4:4,5:5,6:"\u269B\uFE0F"}},cor:{skill:{1:1,2:2,3:3,4:4,5:5,6:"\u{1F41E}"}},vae:{skill:{1:1,2:2,3:3,4:4,5:5,6:"\u{1F98B}"}},t2k:{base:{1:"\u{1F4A5}",2:2,3:3,4:4,5:5,6:6,7:7,8:8,9:9,10:10,11:11,12:12},ammo:{1:"\u{1F4A5}",2:2,3:3,4:4,5:5,6:"\u{1F3AF}"},loc:{1:"L",2:"T",3:"T",4:"T",5:"A",6:"H"}},br:{base:{1:"\u{1F984}",2:2,3:3,4:4,5:5,6:6,7:7,8:8,9:9,10:10,11:11,12:12}}}},GameTypeError=class extends TypeError{constructor(msg){super(`Unknown game: "${msg}". Allowed games are: ${YearZeroRollManager.GAMES.join(", ")}.`),this.name="YZUR | GameType Error"}},DieTermError=class extends TypeError{constructor(msg){super(`Unknown die term: "${msg}". Allowed terms are: ${Object.keys(CONFIG.YZUR.Dice.DIE_TERMS).join(", ")}.`),this.name="YZUR | DieTerm Error"}},YearZeroRoll=class extends Roll{constructor(formula,data={},options={}){options.name==null&&(options.name=data.name),options.game==null&&(options.game=data.game),options.maxPush==null&&(options.maxPush=data.maxPush),super(formula,data,options),this.game||(this.game=CONFIG.YZUR.game??"myz"),options.maxPush!=null&&(this.maxPush=options.maxPush)}get game(){return this.options.game}set game(yzGame){this.options.game=yzGame}get name(){return this.options.name}set name(str){this.options.name=str}set maxPush(n){this.options.maxPush=n;for(let t of this.terms)t instanceof YearZeroDie&&(t.maxPush=n)}get maxPush(){return this.terms.reduce((max,t)=>t instanceof YearZeroDie?Math.max(max,t.maxPush):max,null)}get size(){return this.terms.reduce((s,t)=>t instanceof YearZeroDie?s+t.number:s,0)}get pushCount(){return this.terms.reduce((c,t)=>Math.max(c,t.pushCount||0),0)}get pushed(){return this.pushCount>0}get pushable(){return this.pushCountt.pushable)}get successCount(){return this.terms.reduce((sc,t)=>sc+(t.success??0),0)}get baneCount(){let banableTypes=["base","gear","stress","ammo"],count=0;for(let bt of banableTypes)count+=this.count(bt,1);return count}get attributeTrauma(){return this.count("base",1)}get gearDamage(){return this.count("gear",1)}get stress(){return this.count("stress")}get panic(){return this.count("stress",1)}get mishap(){return!1}get ammoSpent(){let mt=this.getTerms("ammo");return mt.length?mt.reduce((tot,t)=>tot+t.values.reduce((a,b)=>a+b,0),0):0}get hitCount(){return this.count("ammo",6)}get jamCount(){let n=this.count("ammo",1);return n>0?n+this.attributeTrauma:0}get jammed(){return this.pushed?this.jamCount>=2:!1}get baseSuccessQty(){return this.successCount-this.hitCount}get hitLocations(){let lt=this.getTerms("loc");return lt.length?lt.reduce((tot,t)=>tot.concat(t.values),[]):[]}get bestHitLocation(){if(this.hitLocations.length)return Math.max(...this.hitLocations)}static create(formula,data={},options={}){return new YearZeroRoll(formula,data,options)}static forge(dice=[],{title,yzGame=null,maxPush=1}={},options={}){if(yzGame=yzGame??options.game??CONFIG.YZUR?.game,!YearZeroRollManager.GAMES.includes(yzGame))throw new GameTypeError(yzGame);if(!Array.isArray(dice)&&typeof dice=="object"&&!Object.keys(dice).includes("term")){let _dice=[];for(let[term,n]of Object.entries(dice)){if(n<=0)continue;let deno=CONFIG.YZUR.Dice.DIE_TERMS[term].DENOMINATION;deno=CONFIG.Dice.terms[deno].DENOMINATION,_dice.push({term:deno,number:n})}dice=_dice}Array.isArray(dice)||(dice=[dice]);let out=[];for(let d of dice)out.push(YearZeroRoll._getTermFormulaFromBlok(d));let formula=out.join(" + ");YearZeroRoll.validate(formula)||(formula=yzGame==="t2k"?"1d6":"1ds"),options.name==null&&(options.name=title),options.game==null&&(options.game=yzGame),options.maxPush==null&&(options.maxPush=maxPush);let roll=YearZeroRoll.create(formula,{},options);return CONFIG.debug.dice,roll}static createFromDiceQuantities(dice={},{title,yzGame=null,maxPush=1,push=!1}={}){return YearZeroRoll.forge(dice,{title,yzGame,maxPush})}static _getTermFormulaFromBlok(termBlok){let{term,number,flavor,maxPush}=termBlok;return YearZeroRoll.generateTermFormula(number,term,flavor,maxPush)}static generateTermFormula(number,term,flavor="",maxPush=null){let f=`${number}d${term}`;return typeof maxPush=="number"&&(f+=`p${maxPush}`),flavor&&(f+=`[${flavor}]`),f}getTerms(search){return typeof search=="string"?this.terms.filter(t=>t.type===search):this.terms.filter(t=>{let f=!0;if(search.type!=null&&(f=f&&search.type===t.type),search.number!=null&&(f=f&&search.number===t.number),search.faces!=null&&(f=f&&search.faces===t.faces),search.options)for(let key in search.options)f=f&&search.options[key]===t.options[key];if(search.results)for(let key in search.results)f=f&&t.results.some(r=>r[key]===search.results[key]);return f})}count(type,seed=null,comparison="="){return this.terms.reduce((c,t)=>{if(t.type===type)if(t.results.length)for(let r of t.results)r.active&&(seed!=null?comparison===">"?r.result>seed&&c++:comparison===">="?r.result>=seed&&c++:comparison==="<"?r.result0;qty--)term.number++,this._evaluated&&(term.roll(),value!=null&&(term.results[term.results.length-1].result=value));else{let cls=CONFIG.YZUR.Dice.DIE_TERMS[type];term=new cls({number:qty,faces:range2,maxPush:this.maxPush??1,options}),this._evaluated&&(await term.evaluate(),value!=null&&term.results.forEach(r=>r.result=value)),this.terms.length>0&&this.terms.push(new OperatorTerm({operator:type==="neg"?"-":"+"})),this.terms.push(term)}return this._formula=this.constructor.getFormula(this.terms),this._evaluated&&(this._total=this._evaluateTotal()),this}removeDice(qty,search,{discard=!1,disable=!1}={}){if(!qty)return this;for(;qty>0;qty--){let term=this.getTerms(search)[0];if(term){if(term.number--,term.number<=0){let type=search.type??search,index=this.terms.findIndex(t=>t.type===type&&t.number===0);this.terms.splice(index,1),this.terms[index-1]?.operator&&this.terms.splice(index-1,1)}else if(this._evaluated){let index=term.results.findIndex(r=>r.active);if(index<0)break;discard||disable?(discard&&(term.results[index].discarded=discard),disable&&(term.results[index].active=!disable)):term.results.splice(index,1)}}else break}let terms=this.terms;return terms[0]instanceof OperatorTerm&&terms.shift(),this._formula=this.constructor.getFormula(this.terms),this._evaluated&&(this.terms.length?this._total=this._evaluateTotal():this._total=0),this}async push(){return this._evaluated||await this.evaluate(),this.pushable?(this.terms.forEach(t=>t instanceof YearZeroDie?t.push():t),this._evaluated=!1,await this.evaluate(),this):this}async modify(mod=0){if(mod)if(this.game==="t2k"||this.game==="br"){let diceMap=[null,6,8,10,12,1/0],typesMap=["d","d","c","b","a","a"],refactorRange=(range2,n)=>diceMap[diceMap.indexOf(range2)+n],getTypeFromRange=range2=>typesMap[diceMap.indexOf(range2)],_terms=this.getTerms("base"),dice=_terms.flatMap(t=>new Array(t.number).fill(t.faces));if(this.game==="br"){let lowest=Math.min(...dice);if(mod>0)dice.push(lowest);else if(mod<0){let i=dice.indexOf(lowest);dice.splice(i,1)}mod=0}else for(;mod!==0;){let i;if(mod>0?(dice.length<2?(i=1,dice.push(diceMap[1])):(i=dice.indexOf(Math.min(...dice)),dice[i]=refactorRange(dice[i],1)),mod--):(i=dice.indexOf(Math.max(...dice)),dice[i]=refactorRange(dice[i],-1),mod++),dice[i]===1/0)dice[i]=refactorRange(dice[i],-1);else if(dice[i]===null)dice.length>1?dice.splice(i,1):dice[i]=refactorRange(dice[i],1);else if(dice[i]===void 0)throw new Error(`YZUR | YearZeroRoll#modify | dice[${i}] is out of bounds (mod: ${mod})`)}this.removeDice(100,"base");let skilled=_terms.length>1&&dice.length>1;for(let index=0;index0&&this.count("neg")>0;)this.removeDice(1,"skill"),this.removeDice(1,"neg")}else{let skill=this.count("skill");mod<0&&(mod=Math.max(-skill+1,mod)),await this.addDice(mod,"skill")}else return this;if(this._evaluated)for(let t of this.terms)t._evaluated||await t.evaluate();return this}async getTooltip(){let parts=this.dice.map(d=>d.getTooltipData()).sort((a,b)=>{let sorts=CONFIG?.YZUR?.Chat?.diceSorting||YZUR.Chat.diceSorting||[];if(!sorts.length)return 0;let at=sorts.indexOf(a.type),bt=sorts.indexOf(b.type);return at-bt});if(this.pushed)for(let part of parts){let matrix=[],n=part.number,p=this.pushCount;for(;p>=0;p--)matrix[p]=new Array(n).fill(void 0);for(let r of part.rolls){let row=r.row||0,col=r.col||0;matrix[row][col]=r}part.rolls=matrix}return foundry.applications.handlebars.renderTemplate(this.constructor.TOOLTIP_TEMPLATE,{parts,pushed:this.pushed,pushCounts:this.pushed?[...Array(this.pushCount+1).keys()].sort((a,b)=>b-a):void 0,config:CONFIG.YZUR??{},options:this.options})}async getRollInfos(template=null){template=template??CONFIG.YZUR?.Roll?.infosTemplate;let context={roll:this};return foundry.applications.handlebars.renderTemplate(template,context)}async render(chatOptions={}){CONFIG.debug.dice,chatOptions=foundry.utils.mergeObject({user:game.user.id,flavor:this.name,template:this.constructor.CHAT_TEMPLATE,blind:!1},chatOptions);let isPrivate=chatOptions.isPrivate;this._evaluated||await this.evaluate();let chatData={formula:isPrivate?"???":this._formula,flavor:isPrivate?null:chatOptions.flavor,user:chatOptions.user,tooltip:isPrivate?"":await this.getTooltip(),total:isPrivate?"?":Math.round(this.total*100)/100,success:isPrivate?"?":this.successCount,showInfos:isPrivate?!1:CONFIG.YZUR?.Chat?.showInfos,infos:isPrivate?null:await this.getRollInfos(chatOptions.infosTemplate),pushable:isPrivate?!1:this.pushable,options:chatOptions,isPrivate,roll:this};return foundry.applications.handlebars.renderTemplate(chatOptions.template,chatData)}async toMessage(messageData={},{rollMode=null,create=!0}={}){return messageData=foundry.utils.mergeObject({user:game.user.id,speaker:ChatMessage.getSpeaker(),content:this.total},messageData),await super.toMessage(messageData,{rollMode,create})}duplicate(){return this.constructor.fromData(this.toJSON())}},YearZeroRollManager=class{constructor(){throw new SyntaxError(`YZUR | ${this.constructor.name} cannot be instanciated!`)}static register(yzGame,config,options={}){YearZeroRollManager._overrideDiceTermFromData(),YearZeroRollManager.registerConfig(config),YearZeroRollManager._initialize(yzGame),YearZeroRollManager.registerDice(yzGame,options?.index)}static registerConfig(config){CONFIG.YZUR=foundry.utils.mergeObject(YZUR,config)}static registerDice(yzGame,i){if(!yzGame||typeof yzGame!="string")throw new SyntaxError("YZUR | A game must be specified for the registration.");YearZeroRollManager.GAMES.includes(yzGame)||YearZeroRollManager.DIE_TERMS_MAP[yzGame]||(YearZeroRollManager.DIE_TERMS_MAP[yzGame]=[]);let diceTypes=YearZeroRollManager.DIE_TERMS_MAP[yzGame];for(let type of diceTypes)YearZeroRollManager.registerDie(type);YearZeroRollManager.registerRoll(void 0,i)}static registerRoll(cls=YearZeroRoll,i=0){CONFIG.Dice.rolls[i]=cls,CONFIG.Dice.rolls[i].CHAT_TEMPLATE=CONFIG.YZUR.Roll.chatTemplate,CONFIG.Dice.rolls[i].TOOLTIP_TEMPLATE=CONFIG.YZUR.Roll.tooltipTemplate,CONFIG.YZUR.Roll.index=i,i>0&&YearZeroRollManager._overrideRollCreate(i)}static registerDie(term){let cls=CONFIG.YZUR.Dice.DIE_TERMS[term];if(!cls)throw new DieTermError(term);let deno=cls.DENOMINATION;if(!deno)throw new SyntaxError(`YZUR | Undefined DENOMINATION for "${cls.name}".`);let reg=CONFIG.Dice.terms[deno];CONFIG.Dice.terms[deno]=cls}static registerCustomDie(term,data){if(!YearZeroRollManager.GAMES.includes(CONFIG.YZUR.game))throw new GameTypeError("YZUR | Unregistered game. Please register a game before registering a custom die.");let cls=YearZeroRollManager.createDieClass(data);CONFIG.YZUR.Dice.DIE_TERMS[term],CONFIG.YZUR.Dice.DIE_TERMS[term]=cls,YearZeroRollManager.DIE_TERMS_MAP[CONFIG.YZUR.game].push(term),YearZeroRollManager.registerDie(term)}static _initialize(yzGame){if(!CONFIG.YZUR)throw new ReferenceError("YZUR | CONFIG.YZUR does not exists!");CONFIG.YZUR.game,CONFIG.YZUR.game=yzGame}static _overrideDiceTermFromData(){foundry.dice.terms.DiceTerm.prototype.constructor.fromData=function(data){let cls=CONFIG.Dice.termTypes[data.class];if(!cls){let termkeys=Object.keys(CONFIG.Dice.terms),stringifiedFaces=String(data.faces);data.class==="Die"&&termkeys.includes(stringifiedFaces)?(cls=CONFIG.Dice.terms[stringifiedFaces],data.class=cls.name):cls=Object.values(CONFIG.Dice.terms).find(c=>c.name===data.class)||foundry.dice.terms.Die}return cls._fromData(data)}}static _overrideRollCreate(index=1){Roll.prototype.constructor.create=function(formula,data={},options={}){let n=options.yzur??("game"in data||"maxPush"in data||"game"in options||"maxPush"in options||formula.match(/\d*d(:?[bsngzml]|6|8|10|12)/i))?index:0,cls=CONFIG.Dice.rolls[n];return new cls(formula,data,options)}}static createDieClass(data){if(!data||typeof data!="object")throw new SyntaxError("YZUR | To create a Die class, you must pass a DieClassData object!");let{name,denomination:deno,faces,type,lockedValues}=data;if(typeof faces!="number"||faces<=0)throw new DieTermError(`YZUR | Invalid die class faces "${faces}"`);let YearZeroCustomDie=class extends YearZeroDie{constructor(termData={}){termData.faces=faces,super(termData)}};if(!name|typeof name!="string")throw new DieTermError(`YZUR | Invalid die class name "${name}"`);if(Object.defineProperty(YearZeroCustomDie,"name",{value:name}),!deno||typeof deno!="string")throw new DieTermError(`YZUR | Invalid die class denomination "${deno}"`);if(YearZeroCustomDie.DENOMINATION=deno,type!=null){if(typeof type!="string")throw new DieTermError(`YZUR | Invalid die class type "${type}"`);CONFIG.YZUR.Dice.DIE_TYPES.includes(type),CONFIG.YZUR.Icons[CONFIG.YZUR.game][type],YearZeroCustomDie.TYPE=type}if(lockedValues!=null){if(!Array.isArray(lockedValues))throw new DieTermError(`YZUR | Invalid die class locked values "${lockedValues}" (Not an Array)`);for(let[i,v]of lockedValues.entries())if(typeof v!="number")throw new DieTermError(`YZUR | Invalid die class locked value "${v}" at [${i}] (Not a Number)`);YearZeroCustomDie.LOCKED_VALUES=lockedValues}return YearZeroCustomDie}};YearZeroRollManager.DIE_TERMS_MAP={myz:["base","skill","gear","neg"],fbl:["base","skill","gear","neg","artoD8","artoD10","artoD12"],alien:["skill","stress"],tales:["skill"],cor:["skill"],vae:["skill"],t2k:["a","b","c","d","ammo","loc"],br:["brD12","brD10","brD8","brD6"]};YearZeroRollManager.GAMES;Object.defineProperty(YearZeroRollManager,"GAMES",{get:()=>Object.keys(YearZeroRollManager.DIE_TERMS_MAP)});async function getActiveActor(){let actor;if(game.user.isGM&&canvas.ready&&canvas.tokens.controlled.length>1)return chooseActor(canvas.tokens.controlled.map(t=>t.actor),{title:game.i18n.localize("FLBR.MACRO.GetActorTitle"),notes:game.i18n.localize("FLBR.MACRO.GetActorHint")});let speaker=ChatMessage.getSpeaker();if(speaker.token&&(actor=game.actors.tokens[speaker.token]),actor||(actor=game.actors.get(speaker.actor)),!actor){ui.notifications.warn(game.i18n.format("FLBR.MACRO.NoActor",{actor:speaker.alias}));return}return actor}async function chooseActor(actors=[],options={}){if(!actors.length)return;if(actors.length===1)return actors[0];let content='
",options.notes&&(content+=`

${options.notes}

`),content+="
";let actorId=await Dialog.prompt({title:options.title??"Choose an Actor",content,label:game.i18n.localize("FLBR.OK"),callback:html=>html[0].querySelector("form").actor.value,rejectClose:!1,options:{width:400,classes:["blade-runner","dialog"],minimizable:!1}});return actors.find(a=>a.id===actorId)}var BRRollHandler=class extends FormApplication{roll={};messageId;constructor({title="Blade Runner RPG",actor={},items=[],attributeKey=null,skillKey=null,dice=[6],modifier=0,modifiers=[],maxPush=1},options={}){super({},options),this.title=title,this.actor=actor,this.items=Array.isArray(items)?items:[items],this.item=this.items[0],this.attributeKey=attributeKey,this.skillKey=skillKey,this.modifiers=Array.isArray(modifiers)?modifiers:[modifiers],this.items.length>0&&this.modifiers.push(...this.items.flatMap(i=>i.getModifiers({targets:[attributeKey,skillKey]}))),this._modifier=modifier,this.dice=Array.isArray(dice)?dice:[dice],this.dice=this.dice.filter(d=>d>0),this.maxPush=maxPush??1,this.damage=options.damage??this.item?.attacks?.[0]?.damage,this.damageType=options.damageType??this.item?.attacks?.[0]?.damageType,this.crit=options.crit??this.item?.attacks?.[0]?.crit,this.options.sendMessage=options.sendMessage??!0,this.options.unlimitedPush=!!options.unlimitedPush,this.options.disabledPush=!!options.disabledPush}get title(){return this.options.title}set title(str){this.options.title=str}get modifier(){return this._modifier+this.modifiers.reduce((sum,m)=>sum+(m.active?m.value:0),0)}set modifier(val){this._modifier=val}get advantage(){return this.dice.length>2||this.modifier>0}get disadvantage(){return this.dice.length<2||this.modifier<0}get isAttack(){return!!this.damage||[ITEM_TYPES.WEAPON,ITEM_TYPES.EXPLOSIVE].includes(this.item?.type)}get message(){return game.messages.get(this.messageId)}static get defaultOptions(){let sysId=game.system.id||SYSTEM_ID;return foundry.utils.mergeObject(super.defaultOptions,{classes:[sysId,"roll-application"],width:"450",resizable:!1})}get template(){let sysId=game.system.id||SYSTEM_ID;return this.options.template||`systems/${sysId}/templates/components/roll/roller.hbs`}getData(options){return{title:this.title,dice:this.dice,maxPush:this.maxPush,modifier:this.modifier>=0?`+${this.modifier}`:this.modifier,modifiers:this.modifiers,advantage:this.disadvantage?!1:this.advantage,disadvantage:this.advantage?!1:this.disadvantage,attributeKey:this.attributeKey,skillKey:this.skillKey,rollMode:this.options.rollMode??game.settings.get("core","rollMode"),attack:this.isAttack,damage:this.damage,damageType:this.damageType,config:CONFIG.BLADE_RUNNER,options}}createSpeaker(){return{alias:this.options.alias||this.actor?.name,actor:this.options.actorId||this.actor?.id,token:this.options.tokenId||this.actor?.token?.id,scene:this.options.sceneId||this.actor?.token?.parent.id}}static create(data={},options={}){return new this(data,options).render(!0)}static getSpeaker({actor,scene,token}){return scene&&token?game.scenes.get(scene)?.tokens.get(token)?.actor:game.actors.get(actor)}async _updateObject(event,formData){return this._validateForm(event,formData),this._handleFormData(event,formData)}_validateForm(event,formData){if(foundry.utils.isEmpty(formData)||!this.dice.length||this.dice.includes("0")||!["roll","advantage","disadvantage"].includes(event.submitter.id)){let msg=game.i18n.localize("WARNING.NoDiceInput");throw ui.notifications.warn(msg),new Error(msg)}return!0}_handleFormData(event,formData){this.options.rollMode=formData.rollMode;let modifier=this._handleModifier(event.submitter.id);return this.executeRoll(modifier)}_handleModifier(buttonId){switch(buttonId){case"roll":return 0;case"advantage":return this.dice.length>2?0:1;case"disadvantage":return this.dice.length<2?0:-1}return this.modifier}getRollOptions(){let speaker=this.createSpeaker(),unlimitedPush=this.options.unlimitedPush;return{name:this.actor?this.title.replace(`${this.actor.name}: `,""):this.title,maxPush:unlimitedPush?1e3:this.maxPush,attributeKey:this.attributeKey,alias:speaker.alias,actorId:speaker.actor,actorType:this.options.actorType||this.actor?.type,tokenId:speaker.token,sceneId:speaker.scene,damage:this.damage,damageType:this.damageType,damageTypeName:FLBR.damageTypes[this.damageType],crit:this.crit,isExplosive:this.item?.type===ITEM_TYPES.EXPLOSIVE,item:this.item?.name||this.items.map(i=>i.name),itemId:this.item?.id||this.items.map(i=>i.id),yzur:!0}}async executeRoll(modifier=0){if(modifier){let min=Math.min(...this.dice);if(modifier>0)this.dice.push(min);else{let index=this.dice.indexOf(min);this.dice=this.dice.filter((_d,i)=>i!==index)}}let dice=this.dice.map(d=>({term:`${d}`,number:1}));if(this.roll=YearZeroRoll.forge(dice,{},this.getRollOptions()),await this.roll.roll(),this.options.sendMessage){let message=await this.roll.toMessage({speaker:this.createSpeaker()},{rollMode:this.options.rollMode});return this.messageId=message.id,message}return this.roll}static async pushRoll(message,{sendMessage=!0}={}){if(!message||!message.rolls.length)return;let roll=message.rolls[0].duplicate();if(roll.dice.some(d=>d.faces>=10&&d.pushable&&d.values.some(v=>v>=6&&v<10))){let pushSelections=await BRRollHandler.selectPush(roll.dice);if(pushSelections)for(let el of pushSelections){let[x,y]=el.name.split("."),result=roll.dice[x].results.find(r=>r.active&&r.indexResult===Number(y));if(!result)throw new Error(`Push Selection | No result found for index ${el.name}`);!el.disabled&&!el.checked?result.locked=!0:result.locked&&(result.locked=!1)}else return game.messages.directory.updateMessage(message),roll}await roll.push();let flavor=message.flavor,speakerData=message.speaker,speaker=this.getSpeaker(speakerData);return speaker&&await this.updateActor(roll,speaker),sendMessage?(await message.delete(),roll.toMessage({flavor,speaker:speakerData})):roll}static async cancelPush(message){if(!message||!message.rolls.length)return;let roll=message.rolls[0].duplicate();return roll.maxPush=0,await message.update({rolls:[roll.toJSON()]}),message}static async updateActor(roll,speaker){roll.baneCount&&await this.applyDamage(roll,speaker)}static async applyDamage({attributeTrauma,options:{attributeKey}},speaker){let currentDamage=attributeTrauma,nature=speaker?.system.nature;if(!nature)return ui.notifications.error("WARNING.ApplyDamageNoNature",{localize:!0});let cap=FLBR.pushTraumaMap[nature]?.[attributeKey];if(!cap)return ui.notifications.error("WARNING.ApplyDamageNoCapacityKey",{localize:!0});let capacity=speaker?.system[cap];if(!capacity||foundry.utils.isEmpty(capacity))return ui.notifications.error("WARNING.ApplyDamageNoActorCapacity",{localize:!0});let{value,max}=capacity;return value=Math.clamp(value-currentDamage,0,max),value===0&&ui.notifications.info("FLBR.YouAreBroken",{localize:!0}),await speaker.update({[`system.${cap}.value`]:value}),value}static async applyCrit(roll){let actor=await getActiveActor()||game.user.character;if(!actor){let actors=game.actors.filter(a=>a.type===ACTOR_TYPES.CHAR||a.isVehicle);actor=await chooseActor(actors,{title:game.i18n.localize("FLBR.CRIT.CriticalInjury"),notes:game.i18n.localize("FLBR.CRIT.ChooseActor")})}if(actor)return actor.drawCrit(roll.options.damageType,roll.successCount-1,`D${roll.options.crit}`)}activateListeners(html){super.activateListeners(html),html.find("input").on("change",ev=>{ev.preventDefault();let elem=ev.currentTarget,key=elem.name;if(key==null)return;let value=elem.type==="checkbox"?elem.checked:elem.value;return(elem.dataset.dtype==="Number"||elem.type==="number")&&(value=Number(value)),foundry.utils.setProperty(this,key,value),this.render()}),html.find(".add-die").on("click",async ev=>{ev.preventDefault();let elem=ev.currentTarget,lowestDie=Math.min(...this.dice);elem.style.display="none";let die=await BRRollHandler.askDie(lowestDie);if(elem.style.display="flex",!!die)return this.modifiers.forEach(m=>m.active=!1),this.dice.push(die),this.render()}),html.find(".modifiers input[type=checkbox]").on("click",ev=>{ev.preventDefault();let elem=ev.currentTarget,modifierId=elem.dataset.id;if(!modifierId)return;let mod=this.modifiers.find(m=>m.id===modifierId);if(mod)return mod.active=elem.checked,this.render()})}static async waitForRoll(data,options){return new Promise((resolve,reject)=>{let roller=new this(data,options),originalClose=roller.close;roller.close=async opts=>{await originalClose.bind(roller,opts)();let roll=roller.roll;roll instanceof foundry.dice.Roll?(roller.message&&game.dice3d&&game.dice3d.isEnabled()&&await game.dice3d.waitFor3DAnimationByMessageID(roller.messageId),resolve(roll)):reject(new Error("The dialog was closed without a choice being made."))},roller.render(!0)})}static async askDie(lowest=6){let template="systems/blade-runner/templates/components/roll/roll-askdie-dialog.hbs",content=await foundry.applications.handlebars.renderTemplate(template,{lowest});return Dialog.prompt({title:game.i18n.localize("FLBR.ROLLER.AddDie"),content,label:game.i18n.localize("FLBR.OK"),callback:html=>Number(html[0].querySelector("form").diesize.value),rejectClose:!1,options:{width:100,classes:["blade-runner","dialog"],minimizable:!1}})}static async selectPush(dice){return Dialog.wait({title:game.i18n.localize("FLBR.ROLLER.SelectDiceToPush"),content:await foundry.applications.handlebars.renderTemplate("systems/blade-runner/templates/components/roll/roll-push-select-dialog.hbs",{dice}),buttons:{ok:{label:game.i18n.localize("YZUR.CHAT.ROLL.Push"),icon:'',callback:html=>html[0].querySelector("form")},cancel:{label:game.i18n.localize("FLBR.Cancel"),icon:'',callback:()=>!1}}},{},{classes:["blade-runner","dialog","dice-push-select"]})}};var BladeRunnerDialog=class extends Dialog{static get defaultOptions(){let sysId=game.system.id||SYSTEM_ID;return foundry.utils.mergeObject(super.defaultOptions,{classes:[sysId,"dialog"]})}activateListeners(html){super.activateListeners(html),html.find("input").focus(ev=>ev.currentTarget.select()),html.find("input[type=range]").change(event=>{event.preventDefault();let elem=event.currentTarget,span=elem.nextElementSibling;span.innerHTML=elem.value})}static async rangePicker({value=1,min=1,max,title,description}){max=max??value;let template=`systems/${SYSTEM_ID}/templates/components/dialog/range-picker-dialog.hbs`,content=await foundry.applications.handlebars.renderTemplate(template,{value,min,max,description});return this.prompt({title,content,label:game.i18n.localize("FLBR.OK"),callback:html=>html[0].querySelector("form").chooser.value,rejectClose:!1})}static async choose(choices,title,{content,icon="fa-solid fa-play"}={}){if(choices.length<=1)return choices[0][0];let buttons=choices.reduce((btns,[id,label])=>(btns[id]={label,icon:``,callback:()=>id},btns),{});return this.wait({title,content,buttons,default:choices[0][0]},{},{classes:[SYSTEM_ID,"dialog","choice-dialog"]})}static async drawTable(tables,options={}){let template=`systems/${SYSTEM_ID}/templates/components/dialog/table-draw-dialog.hbs`,content=await foundry.applications.handlebars.renderTemplate(template,{formula:options.formula,qty:options.qty||1,selected:options.defaultSelected,disableSelection:options.disableSelection,disableFormula:options.disableFormula,hideFormula:options.disableFormula&&!options.formula,tables}),form=await Dialog.prompt({title:options.title??"Draw From Table",label:game.i18n.localize("FLBR.OK"),content,callback:html=>html[0].querySelector("form"),options:{classes:[SYSTEM_ID,"dialog","table-draw-dialog"]}});if(!form)return;let table=game.tables.get(form.table.value),formula=form.formula?.value||table.formula,drawOptions={displayChat:options.displayChat,rollMode:options.rollMode};formula&&(drawOptions.roll=new Roll(formula));let n=Number(form.qty.value)||1;return n>1?table.drawMany(n,drawOptions):table.draw(drawOptions)}};function addChatMessageContextOptions(_html,options){let canApplyDamage=li=>game.user.isGM&&game.user.targets.size&&!!li.querySelector(".chat-card[data-damage]");return options.push({name:game.i18n.localize("FLBR.CHAT_ACTION.ApplyDamage"),icon:FLBR.Icons.buttons.attack,condition:canApplyDamage,callback:li=>distributeDamageFromMessage(li.dataset.messageId)}),options}async function distributeDamageFromMessage(messageId){let roll=game.messages.get(messageId).rolls[0],damageType=roll.options.damageType,s=roll.successCount;if(!s)return;let boom=roll.options.isExplosive,defenderTokens=game.user.targets;for(let defenderToken of defenderTokens){if(defenderToken.actor.type===ACTOR_TYPES.LOOT){ui.notifications.info(game.i18n.format("FLBR.COMBAT.SkippedDefender",{name:`${defenderToken.name}`})+" ("+game.i18n.localize("FLBR.COMBAT.CannotBeDamaged")+")");continue}if(!s)break;let n=s;if(defenderTokens.size>1&&!boom)if(n=await BladeRunnerDialog.rangePicker({title:game.i18n.localize("FLBR.DIALOG.AssignSuccesses"),description:game.i18n.format("FLBR.DIALOG.AssignSuccessesHint",{name:`${defenderToken.name}`}),value:s}),n)s-=n;else continue;let actor=defenderToken.actor,damage=n>0?(roll.options.damage||0)+1*(n-1):0;await actor.applyDamage(damage);let brokenByDamage=actor.isBroken&&[DAMAGE_TYPES.CRUSHING,DAMAGE_TYPES.PIERCING].includes(damageType);if(roll.successCount>=2||brokenByDamage)try{await actor.drawCrit(damageType,roll.successCount-1,`D${roll.options.crit}`)}catch{}}}function hideChatActionButtons(html){let chatCard=html.querySelector(".yzur.chat-card");if(!chatCard)return;let actor=game.actors.get(chatCard.getAttribute("data-actor-id")),buttons=chatCard.querySelectorAll("button");for(let btn of buttons)actor&&!actor.isOwner&&(btn.style.display="none")}function addChatListeners(html){html.addEventListener("click",ev=>{ev.target.closest(".blade-runner-display-manual")&&game.bladerunner.macros.displayManual.call(ev.target,ev)}),html.addEventListener("click",ev=>{ev.target.closest(".roll-button")&&_onRollAction.call(ev.target,ev)}),html.addEventListener("click",ev=>{ev.target.closest(".crit-roll")&&_onCritRoll.call(ev.target,ev)})}async function _onRollAction(event){event.preventDefault();let button=event.target;button.disabled=!0;let messageId=button.closest(".chat-message").dataset.messageId,message=game.messages.get(messageId);switch(button.dataset.action){case"push":await BRRollHandler.pushRoll(message);break;case"cancel-push":await BRRollHandler.cancelPush(message);break;default:break}button.disabled=!1}function _onCritRoll(event){event.preventDefault();let messageId=event.target.closest(".chat-message").dataset.messageId,roll=game.messages.get(messageId)?.rolls[0];return BRRollHandler.applyCrit(roll)}function createBladeRunnerMacro(data,slot){if(data.type==="Stat")return _createBladeRunnerStatMacro(data,slot),!1;if(data.type==="Action")return _createBladeRunnerActionMacro(data,slot),!1;if(data.type==="Item"&&typeof data.uuid=="string"){if(!data.uuid.includes("Actor")&&!data.uuid.includes("Token"))return;let item=foundry.utils.fromUuidSync(data.uuid);return!item||!item.rollable?void 0:(_createBladeRunnerItemMacro(item,slot),!1)}}async function setupMacroFolder(){if(!game.user.isGM)return;let folderName=FLBR.systemMacroFolder;game.folders.filter(f=>f.type==="Macro").find(f=>f.name===folderName)||await Folder.create({name:folderName,type:"Macro",parent:null})}async function _createBladeRunnerStatMacro(data,slot){let folder=game.folders.find(f=>f.type==="Macro"&&f.name===FLBR.systemMacroFolder),command=`game.bladerunner.macros.rollStat("${data.attribute}"`+(data.skill?`, "${data.skill}"`:"")+");";if(!await foundry.utils.fromUuid(data.uuid))return;let commandName=game.i18n.format("FLBR.MACRO.RollStat",{stat:game.i18n.localize(data.skill?`FLBR.SKILL.${data.skill.capitalize()}`:`FLBR.ATTRIBUTE.${data.attribute.toUpperCase()}`)}),macro=findMacro(commandName,command);macro||(macro=await Macro.create({name:commandName,type:"script",img:"icons/svg/dice-target.svg",command,flags:{"bladerunner.statMacro":!0},folder:folder.id,"ownership.default":CONST.DOCUMENT_OWNERSHIP_LEVELS.OBSERVER})),game.user.assignHotbarMacro(macro,slot)}async function _createBladeRunnerActionMacro(data,slot){let folder=game.folders.find(f=>f.type==="Macro"&&f.name===FLBR.systemMacroFolder),command=`game.bladerunner.macros.rollAction("${data.action}");`;if(!await foundry.utils.fromUuid(data.uuid))return;let action=FLBR.Actions.find(a=>a.id===data.action),commandName=game.i18n.format("FLBR.MACRO.RollAction",{action:game.i18n.localize(action?.label)}),macro=findMacro(commandName,command);macro||(macro=await Macro.create({name:commandName,type:"script",img:"icons/svg/dice-target.svg",command,flags:{"bladerunner.actionMacro":!0},folder:folder.id,"ownership.default":CONST.DOCUMENT_OWNERSHIP_LEVELS.OBSERVER})),game.user.assignHotbarMacro(macro,slot)}async function _createBladeRunnerItemMacro(item,slot){let folder=game.folders.find(f=>f.type==="Macro"&&f.name===FLBR.systemMacroFolder),command=`game.bladerunner.macros.rollItem("${item.name}");`,macro=findMacro(item.name,command);macro||(macro=await Macro.create({name:item.name,type:"script",img:item.img,command,flags:{"bladerunner.itemMacro":!0},folder:folder.id,"ownership.default":CONST.DOCUMENT_OWNERSHIP_LEVELS.OBSERVER})),game.user.assignHotbarMacro(macro,slot)}async function rollStat(attributeKey,skillKey,options){let actor=await getActiveActor();if(actor)return actor.rollStat(attributeKey,skillKey,options)}async function rollAction(actionKey){return game.bladerunner.actions.has(actionKey)?game.bladerunner.actions.get(actionKey).execute(await getActiveActor()):ui.notifications.error(`Action "${actionKey}" not found!`)}async function rollItem(itemName){let actor=await getActiveActor(),items=actor?actor.items.filter(i=>i.name===itemName):[];if(items.length>1)ui.notifications.warn(game.i18n.format("FLBR.MACRO.MultipleItems",{actor:actor.name,item:itemName}));else if(items.length===0)return ui.notifications.warn(game.i18n.format("FLBR.MACRO.NoItem",{actor:actor.name,item:itemName}));return items[0].roll()}function showRollDialog(data={}){return game.bladerunner.roller.create({title:"Generic Roll",actor:game.user.character,attributeKey:null,skillKey:null,dice:[],maxPush:1,...data})}function findMacro(commandName,command){return game.macros.find(m=>m.name===commandName&&m.command===command&&(m.author===game.user.id||m.ownership.default>=CONST.DOCUMENT_OWNERSHIP_LEVELS.OBSERVER||m.ownership[game.user.id]>=CONST.DOCUMENT_OWNERSHIP_LEVELS.OBSERVER))}async function enrichTextFields(sheetData,fieldNames){for(let fieldName of fieldNames)foundry.utils.hasProperty(sheetData,fieldName)&&foundry.utils.setProperty(sheetData,fieldName,await foundry.applications.ux.TextEditor.enrichHTML(foundry.utils.getProperty(sheetData,fieldName)))}var _ActorSheetConfig=class extends foundry.applications.apps.DocumentSheetConfig{get title(){return`${this.document.name} - ${super.title}`}async _preparePartContext(partId,context,options){let data=await super._preparePartContext(partId,context,options);return partId==="character"&&(data.types=FLBR.characterSubtypes,data.isGM=game.user.isGM),data}static async myFormHandler(_event,_form,formData){let updateData=foundry.utils.expandObject(formData.object);await this.document.update(updateData);let{object}=formData,{documentName,type=CONST.BASE_DOCUMENT_TYPE}=this.document,themes=game.settings.get("core","sheetThemes"),defaultTheme=foundry.utils.getProperty(themes,`defaults.${documentName}.${type}`),documentTheme=themes.documents?.[this.document.uuid],themeChanged=object.defaultTheme!==defaultTheme||object.theme!==documentTheme;themeChanged&&(foundry.utils.setProperty(themes,`defaults.${documentName}.${type}`,object.defaultTheme),themes.documents??={},themes.documents[this.document.uuid]=object.theme,await game.settings.set("core","sheetThemes",themes));let{defaultClass}=this.constructor.getSheetClassesForSubType(documentName,type),sheetClass=this.document.getFlag("core","sheetClass")??"",defaultSheetChanged=object.defaultClass!==defaultClass,documentSheetChanged=object.sheetClass!==sheetClass;if(themeChanged||game.user.isGM&&defaultSheetChanged){if(game.user.isGM&&defaultSheetChanged){let setting=game.settings.get("core","sheetClasses");foundry.utils.setProperty(setting,`${documentName}.${type}`,object.defaultClass),await game.settings.set("core","sheetClasses",setting)}if(!documentSheetChanged)return this.document._onSheetChange({sheetOpen:!0})}if(documentSheetChanged)return this.document.setFlag("core","sheetClass",object.sheetClass)}},ActorSheetConfig=_ActorSheetConfig;__publicField(ActorSheetConfig,"DEFAULT_OPTIONS",{tag:"form",form:{handler:_ActorSheetConfig.myFormHandler,submitOnChange:!1,closeOnSubmit:!0}}),__publicField(ActorSheetConfig,"PARTS",{character:{classes:["standard-form"],template:`systems/${SYSTEM_ID}/templates/actor/actor-sheet-config.hbs`},form:{classes:["standard-form"],template:"templates/sheets/document-sheet-config.hbs"},footer:{template:"templates/generic/form-footer.hbs"}});var BladeRunnerActorSheet=class extends foundry.appv1.sheets.ActorSheet{static get defaultOptions(){return foundry.utils.mergeObject(super.defaultOptions,{dragDrop:[{dragSelector:".embedded-items .embedded-item",dropSelector:null},{dragSelector:".stat-roll[data-attribute]",dropSelector:null}]})}get template(){return`systems/${game.system.id||SYSTEM_ID}/templates/actor/${this.actor.type}/${this.actor.type}-sheet.hbs`}get rollData(){return this.actor.getRollData()}async getData(options){let isOwner=this.actor.isOwner,showEffects=game.settings.get(SYSTEM_ID,SETTINGS_KEYS.USE_ACTIVE_EFFECTS),baseData=super.getData(options),sheetData={cssClass:isOwner?"editable":"locked",owner:isOwner,limited:this.actor.limited,editable:this.isEditable,options:this.options,isGM:game.user.isGM,actor:baseData.actor,system:foundry.utils.duplicate(baseData.actor.system),items:[...this.actor.items].sort((a,b)=>(a.sort||0)-(b.sort||0)),showEffects,effects:showEffects?[...this.actor.effects]:null,rollData:this.rollData,config:CONFIG.BLADE_RUNNER};return await enrichTextFields(sheetData,["system.description"]),sheetData}async _onDropItemCreate(itemData){let type=itemData.type,alwaysAllowedItems=FLBR.physicalItems,allowedItems=FLBR.allowedItems,allowed=!0;if(alwaysAllowedItems.includes(type)||allowedItems[this.actor.type].includes(type)||(allowed=!1),!allowed){let msg=game.i18n.format("FLBR.ActorSheet.NotifWrongItemType",{type:game.i18n.localize(`TYPES.Item.${type}`),actor:game.i18n.localize(`TYPES.Actor.${this.actor.type}`)});return ui.notifications.warn(msg),!1}return super._onDropItemCreate(itemData)}_onDragStart(event){let data=event.currentTarget.dataset;if(typeof data.attribute<"u"||typeof data.skill<"u"||typeof data.action<"u"){let dragData={actorId:this.actor.id,sceneId:this.actor.isToken?canvas.scene?.id:null,tokenId:this.actor.isToken?this.actor.token.id:null,uuid:this.actor.uuid,type:data.action?"Action":"Stat",attribute:data.attribute,skill:data.skill,action:data.action};return event.dataTransfer.setData("text/plain",JSON.stringify(dragData))}return super._onDragStart(event)}_onConfigureSheet(event){event.preventDefault(),new ActorSheetConfig({document:this.actor,position:{top:this.position.top+40,left:this.position.left+(this.position.width-400)/2}}).render(!0)}_getHeaderButtons(){let originalButtons=super._getHeaderButtons();return!game.user.isGM&&this.actor.limited||this.actor.type===ACTOR_TYPES.LOOT?originalButtons:[{label:game.i18n.localize("FLBR.SHEET_HEADER.GenericRoll"),class:"custom-roll",icon:"fas fa-dice",onclick:()=>this.actor.roll()}].concat(originalButtons)}activateListeners(html){if(super.activateListeners(html),!game.user.isGM&&this.actor.limited||!this.isEditable)return;html.find("input").focus(ev=>ev.currentTarget.select()),html.find(".action-roll").click(this._onActionRoll.bind(this)),html.find(".capacities .capacity-boxes").on("click contextmenu",this._onCapacityIncrease.bind(this)),html.find(".item-create").click(this._onItemCreate.bind(this)),html.find(".item-delete").click(this._onItemDelete.bind(this)),html.find(".item-delete-confirmed").click(this._onItemDeleteConfirmed.bind(this)),html.find(".item-control").click(this._onItemControl.bind(this)),html.find(".embedded-item").on("contextmenu",this._onItemEdit.bind(this)),game.settings.get(SYSTEM_ID,SETTINGS_KEYS.USE_ACTIVE_EFFECTS)&&(html.find(".add-active-effect").click(this._onAddActiveEffect.bind(this)),html.find(".active-effect-controls .btn").click(this._onActiveEffectAction.bind(this)))}_onActionRoll(event){event.preventDefault();let actionKey=event.currentTarget.dataset.action;return game.bladerunner.actions.get(actionKey)?.execute(this.actor)}_onItemCreate(event){event.preventDefault();let type=event.currentTarget.dataset.type,itemData={name:game.i18n.format("FLBR.NewItem",{type:game.i18n.localize(`TYPES.Item.${type}`)}),type};return this.actor.createEmbeddedDocuments("Item",[itemData]).then(itmData=>{let itemId=itmData[0].id;this.actor.items.get(itemId).sheet.render(!0)})}_onItemDelete(event){event.preventDefault();let elem=event.currentTarget;elem.style.display="none";let deleteButton=elem.closest(".embedded-item").getElementsByClassName("item-delete-confirmed")[0];deleteButton.style.display="inline"}_onItemDeleteConfirmed(event){event.preventDefault();let itemId=event.currentTarget.closest(".embedded-item").dataset.itemId;return this.actor.deleteEmbeddedDocuments("Item",[itemId])}_onItemEdit(event){return event.preventDefault(),event.currentTarget.dataset.action="edit",this._onItemControl(event)}_onItemControl(event){event.preventDefault();let elem=event.currentTarget,itemId=elem.closest(".embedded-item").dataset.itemId,item=this.actor.items.get(itemId);if(item)switch(elem.dataset.action){case"edit":return item.sheet.render(!0);case"roll":return item.roll();case"blast":return item._rollExplosive();case"mount":return item.update({"system.mounted":!0});case"unmount":return item.update({"system.mounted":!1});case"chat":return item.toMessage()}}_onCapacityIncrease(event){return this._onCapacityChange(event,1)}_onCapacityDecrease(event){return this._onCapacityChange(event,-1)}_onCapacityChange(event,mod=1){event.preventDefault();let elem=event.currentTarget,min=+elem.dataset.min,max=+elem.dataset.max||10,field=elem.dataset.field;if(!field)return;let count=foundry.utils.getProperty(this.actor,field)??0;return event.type==="click"?count+=mod:count-=mod,count=Math.clamp(count,min,max),this.actor.update({[field]:count})}_onAddActiveEffect(event){return event.preventDefault(),this.actor.createEmbeddedDocuments("ActiveEffect",[{label:game.i18n.localize("FLBR.ACTIVE_EFFECT.New"),icon:"icons/svg/aura.svg",origin:this.actor.uuid}])}_onActiveEffectAction(event){event.preventDefault();let effectId=event.currentTarget.closest("[data-effect-id]").dataset.effectId,effect=this.actor.effects.get(effectId);if(effect)switch(event.currentTarget.dataset.action){case"toggle":return effect.update({disabled:!effect.disabled});case"edit":return effect.sheet.render(!0);case"delete":return effect.delete()}}};var BladeRunnerCharacterSheet=class extends BladeRunnerActorSheet{static get defaultOptions(){let sysId=game.system.id||SYSTEM_ID;return foundry.utils.mergeObject(super.defaultOptions,{classes:[sysId,"sheet","actor","character"],width:525,height:616,resizable:!0,scrollY:[".sheet-body"],tabs:[{navSelector:".sheet-tabs",contentSelector:".sheet-body",initial:"stats"}]})}get template(){let sysId=game.system.id||SYSTEM_ID;return!game.user.isGM&&this.actor.limited?`systems/${sysId}/templates/actor/${this.actor.type}/${this.actor.type}-limited-sheet.hbs`:`systems/${sysId}/templates/actor/${this.actor.type}/${this.actor.type}-sheet.hbs`}async getData(options){let sheetData=await super.getData(options);return sheetData.isPC=this.actor.system.subtype===ACTOR_SUBTYPES.PC,sheetData.isNPC=this.actor.system.subtype===ACTOR_SUBTYPES.NPC,sheetData.driving=this.actor.skills.driving?.value,sheetData.actions=game.bladerunner.actions.filter(a=>a.actorType===this.actor.type),sheetData.editNature=game.settings.get(SYSTEM_ID,SETTINGS_KEYS.EDIT_NATURE_PERMISSION)||sheetData.isGM,this.actor.system.subtype===ACTOR_SUBTYPES.PC&&await enrichTextFields(sheetData,["system.bio.keyMemory","system.bio.keyRelationship","system.bio.home","system.bio.appearance"]),sheetData}activateListeners(html){super.activateListeners(html),!(!game.user.isGM&&this.actor.limited)&&this.isEditable&&(html.find(".stat-roll").click(this._onStatRoll.bind(this)),html.find(".meta-currencies .capacity-boxes").on("click contextmenu",super._onCapacityIncrease.bind(this)),html.find(".capacity-resolve .capacity-boxes").on("contextmenu",this._onResolveDecrease.bind(this)),this.actor.isOwner&&html.find(".action-roll[data-action]").each((_index,elem)=>{elem.setAttribute("draggable",!0),elem.addEventListener("dragstart",ev=>this._onDragStart(ev),!1)}))}_onStatRoll(event){event.preventDefault();let elem=event.currentTarget,attrKey=elem.dataset.attribute,skillKey=elem.dataset.skill;return this.actor.rollStat(attrKey,skillKey)}_onResolveDecrease(event){if(event.preventDefault(),this.actor.system.subtype===ACTOR_SUBTYPES.PC&&!(this.actor.system.resolve.value>1))return this.actor.rollResolve()}};var BladeRunnerLootSheet=class extends BladeRunnerActorSheet{static get defaultOptions(){let sysId=game.system.id||SYSTEM_ID;return foundry.utils.mergeObject(super.defaultOptions,{classes:[sysId,"sheet","actor","loot"],width:525,height:616,resizable:!0})}get template(){return`systems/${game.system.id||SYSTEM_ID}/templates/actor/${ACTOR_TYPES.LOOT}/${ACTOR_TYPES.LOOT}-sheet.hbs`}};var BladeRunnerVehicleSheet=class extends BladeRunnerActorSheet{get vehicle(){return this.actor}static get defaultOptions(){let sysId=game.system.id||SYSTEM_ID,opts=foundry.utils.mergeObject(super.defaultOptions,{classes:[sysId,"sheet","actor","vehicle"],width:569,height:700,resizable:!0,scrollY:[".sheet-body"],tabs:[{navSelector:".sheet-tabs",contentSelector:".sheet-body",initial:"combat"}]});return opts.dragDrop.push({dragSelector:".vehicle-seat.occupied img",dropSelector:null}),opts}async getData(options){let sheetData=await super.getData(options),[otherItems,mountedWeapons]=sheetData.items.partition(i=>!!i.system.mounted);return sheetData.trunk=otherItems,sheetData.mountedWeapons=mountedWeapons,sheetData.crew=this.vehicle.crew.contents,sheetData.actions=game.bladerunner.actions.filter(a=>a.actorType===this.actor.type),sheetData}_onDragStart(event){super._onDragStart(event);let elem=event.currentTarget.closest(".vehicle-seat");if(!elem)return;let actor=this.vehicle.crew.get(elem.dataset.occupantId);if(!actor)return;let dragData=actor.toDragData();dragData.parent=this.vehicle.id,event.dataTransfer.setData("text/plain",JSON.stringify(dragData))}async _onDropActor(event,data){if(await super._onDropActor(event,data),!this.vehicle.isOwner)return;let actor=await foundry.utils.fromUuid(data.uuid);if(actor)return data.parent===this.vehicle.id?this._sortCrew(event,actor):actor.type===ACTOR_TYPES.CHAR?this.dropCrew(actor):ui.notifications.info(`FLBR.VEHICLE.NotifDropCrew${actor.type.capitalize()}`,{localize:!0})}async _sortCrew(event,newOccupant){if(this.vehicle.crew.size<=1)return;let crew=this.vehicle.system.crew,newOccupantId=newOccupant.id;crew.splice(crew.findIndex(o=>o.id===newOccupantId),1);let targetOccupantId=event.target.closest(".vehicle-seat.occupied")?.dataset.occupantId;return targetOccupantId?crew.splice(crew.findIndex(o=>o.id===targetOccupantId),0,{id:newOccupantId}):crew.push({id:newOccupantId}),this.vehicle.update({"system.crew":crew})}async dropCrew(actor){if(this.vehicle.crew.full)return ui.notifications.info("FLBR.VEHICLE.NotifCrewFull",{localize:!0});if((actor.system.subtype===ACTOR_SUBTYPES.NPC||!actor.prototypeToken.actorLink)&&await Dialog.confirm({title:game.i18n.localize("FLBR.VEHICLE.CreateActor"),content:game.i18n.format("FLBR.VEHICLE.CreateActorHint",{name:actor.name})})){let actorData=actor.toObject();actorData.prototypeToken.actorLink=!0,actorData.name=`[${this.vehicle.name}] ${game.i18n.format("DOCUMENT.CopyOf",{name:actor.name})}`,actor=await Actor.create(actorData)}await this.vehicle.addVehicleOccupant(actor)}activateListeners(html){super.activateListeners(html),!(!game.user.isGM&&this.actor.limited)&&this.isEditable&&(html.find(".mvr-roll").click(this._onManeuverabilityRoll.bind(this)),html.find(".roll-vehicle-armor").click(this._onVehicleArmorRoll.bind(this)),new foundry.applications.ux.ContextMenu.implementation(html[0],".hull",[{name:game.i18n.localize("FLBR.VEHICLE.IncreaseHull"),icon:FLBR.Icons.buttons.plus,callback:()=>this.vehicle.update({"system.hull.max":this.vehicle.system.hull.max+1}),condition:()=>this.vehicle.system.hull.maxthis.vehicle.update({"system.hull.max":this.vehicle.system.hull.max-1}),condition:()=>this.vehicle.system.hull.max>1}],{jQuery:!0}),html.find(".vehicle-seat:not(.empty):not(.broken)").click(this._onCrewAction.bind(this)),new foundry.applications.ux.ContextMenu.implementation(html[0],".vehicle-seat:not(.button)",[{name:game.i18n.localize("FLBR.VEHICLE.EditPassenger"),icon:FLBR.Icons.buttons.edit,callback:el=>game.actors.get(el.data("occupant-id"))?.sheet.render(!0),condition:el=>el.hasClass("occupied")},{name:game.i18n.localize("FLBR.VEHICLE.RemovePassenger"),icon:FLBR.Icons.buttons.delete,callback:el=>this.vehicle.removeVehicleOccupant(el.data("occupant-id")),condition:el=>el.hasClass("occupied")},{name:game.i18n.localize("FLBR.VEHICLE.DeleteSeat"),icon:FLBR.Icons.buttons.remove,callback:()=>this.vehicle.update({"system.passengers":this.vehicle.system.passengers-1}),condition:()=>!this.vehicle.crew.full&&this.vehicle.system.passengers>1}],{jQuery:!0}),this.actor.isOwner)}async _onActionRoll(event){event.preventDefault();let actionKey=event.currentTarget.dataset.action,action=game.bladerunner.actions.get(actionKey);if(action)if(action.onCrew){let actor=await this.vehicle.crew.choose();return actor?action.execute(actor):void 0}else return action.execute(this.vehicle)}_onCrewAction(event){event.preventDefault();let elem=event.currentTarget,action=elem.dataset.action;if(action==="add-occupant")return this.vehicle.update({"system.passengers":this.vehicle.system.passengers+1});if(action==="roll-occupant")return this.vehicle.crew.get(elem.dataset.occupantId)?.rollStat(ATTRIBUTES.VEHICLE_MANEUVERABILITY,SKILLS.DRIVING)}_onManeuverabilityRoll(event){return event.preventDefault(),this.vehicle.roll({title:`${this.vehicle.name}: ${game.i18n.localize("FLBR.ATTRIBUTE.MVR")}`,dice:[this.vehicle.system.maneuverability]})}_onVehicleArmorRoll(event){return event.preventDefault(),this.vehicle.roll({title:`${this.vehicle.name}: ${game.i18n.localize("FLBR.ItemArmor")}`,dice:new Array(2).fill(this.vehicle.system.armor),maxPush:0},{disabledPush:!0})}};var ItemAction=class{constructor(type,{name,item,attribute,skill,macro}){switch(this.id=foundry.utils.randomID(8),this.type=type||ItemAction.Types.ROLL_SKILL,this.name=name,this.item=item,this.type){case ItemAction.Types.ROLL_SKILL:this.attribute=attribute||"str",this.skill=skill||"closeCombat";break;case ItemAction.Types.RUN_MACRO:this.macro=macro}}get actor(){return this.item?.actor}get title(){switch(this.type){case ItemAction.Types.ROLL_SKILL:return this.name+" ("+game.i18n.localize(`FLBR.ATTRIBUTE.${this.attribute.toUpperCase()}`)+" + "+game.i18n.localize(`FLBR.SKILL.${this.skill.capitalize()}`)+")";default:return this.name}}toObject(){return{type:this.type,name:this.name,attribute:this.attribute,skill:this.skill,macro:this.macro}}};ItemAction.Types={ROLL_SKILL:"skill",RUN_MACRO:"macro"};var ItemAttack=class{constructor({name,item,damage,damageType,crit,range:range2}){this.id=foundry.utils.randomID(8),this.name=name,this.item=item,this.damage=damage??1,this.damageType=damageType??2,this.crit=crit||6,this.range={min:range2?.min??0,max:range2?.max??1}}get actor(){return this.item?.actor}get title(){return`${this.name}
[ ${game.i18n.localize("FLBR.WeaponDamage")}: ${this.damage} ${game.i18n.localize(CONFIG.BLADE_RUNNER.damageTypes[this.damageType])},  ${game.i18n.localize("FLBR.CRIT.Crit")}: ${game.i18n.localize("FLBR.D")}${this.crit},  ${game.i18n.localize("FLBR.Range")}: ${game.i18n.localize(CONFIG.BLADE_RUNNER.ranges[this.range.max])} ]`}toObject(){return{name:this.name,damage:this.damage,damageType:this.damageType,crit:this.crit,range:this.range}}};var BladeRunnerItemSheet=class extends foundry.appv1.sheets.ItemSheet{collapsibleStates={};static get defaultOptions(){let sysId=game.system.id||SYSTEM_ID;return foundry.utils.mergeObject(super.defaultOptions,{classes:[sysId,"sheet","item"],width:250,scrollY:[".sheet-body"],resizable:!1,tabs:[{navSelector:".sheet-tabs",contentSelector:".sheet-body",initial:"info"}],top:100})}get template(){return`systems/${game.system.id||SYSTEM_ID}/templates/item/item-sheet.hbs`}async getData(options){let showEffects=game.settings.get(SYSTEM_ID,SETTINGS_KEYS.USE_ACTIVE_EFFECTS),baseData=super.getData(options),sheetData={cssClass:this.isEditable?"editable":"locked",owner:!!this.item.actor?.isOwner,editable:this.isEditable,options:this.options,isGM:game.user.isGM,inActor:!!this.item.actor,isOffensive:this.item.isOffensive,isConsumable:this.item.isConsumable,inVehicle:this.item.actor?.isVehicle,item:baseData.item,system:foundry.utils.duplicate(baseData.item.system),showEffects,effects:showEffects?this.item.effects.contents:null,rollable:this.item.rollable,rollData:this.item.getRollData(),collapsibleStates:this.collapsibleStates,config:CONFIG.BLADE_RUNNER};return await enrichTextFields(sheetData,["system.description"]),sheetData}_getHeaderButtons(){let originalButtons=super._getHeaderButtons(),myButtons=[];return this.item.hasAction&&this.item.actor&&myButtons.push({label:game.i18n.localize("FLBR.SHEET_HEADER.ItemRoll"),class:"item-roll",icon:"fas fa-dice",onclick:()=>this.item.roll()}),this.item.type===ITEM_TYPES.ARMOR?myButtons.push({label:game.i18n.localize("FLBR.SHEET_HEADER.ArmorRoll"),class:"item-roll",icon:"fas fa-shield-alt",onclick:()=>this.item.roll()}):this.item.type===ITEM_TYPES.EXPLOSIVE&&myButtons.push({label:game.i18n.localize("FLBR.SHEET_HEADER.BlastPowerRoll"),class:"blast-roll",icon:"fas fa-bomb",onclick:()=>this.item._rollExplosive()}),myButtons.push({label:game.i18n.localize("FLBR.SHEET_HEADER.ItemPost"),class:"item-post",icon:"far fa-comment-dots",onclick:()=>this.item.toMessage()}),myButtons.concat(originalButtons)}activateListeners(html){if(super.activateListeners(html),this._setupAccordions(),!game.user.isGM&&this.actor.limited||!this.isEditable)return;let inputs=html.find("input");inputs.focus(ev=>ev.currentTarget.select()),inputs.addBack().find('[data-dtype="Number"]').change(this._onChangeInputDelta.bind(this)),html.find(".add-action").click(this._onAddItemAction.bind(this)),html.find(".delete-action").click(this._onDeleteItemAction.bind(this)),html.find(".add-attack").click(this._onAddItemAttack.bind(this)),html.find(".delete-attack").click(this._onDeleteItemAttack.bind(this)),html.find(".add-modifier").click(this._onAddModifier.bind(this)),html.find(".delete-modifier").click(this._onDeleteModifier.bind(this)),game.settings.get(SYSTEM_ID,SETTINGS_KEYS.USE_ACTIVE_EFFECTS)&&(html.find(".add-active-effect").click(this._onAddActiveEffect.bind(this)),html.find(".active-effect-controls .btn").click(this._onActiveEffectAction.bind(this))),this.item.type===ITEM_TYPES.EXPLOSIVE&&html.find(".item-property-blast .score-selector").change(this._onBlastChange.bind(this))}_onChangeInputDelta(event){event.preventDefault();let input=event.target,value=input.value;if(value[0]==="+"||value[0]==="-"){let delta=parseFloat(value);input.value=foundry.utils.getProperty(this.item,input.name)+delta}else value[0]==="="&&(input.value=value.slice(1))}_onAddItemAction(event){event.preventDefault();let itemAction=new ItemAction(null,{name:game.i18n.localize("FLBR.ItemNewAction")});return this.item.update({[`system.actions.${itemAction.id}`]:itemAction.toObject()})}_onDeleteItemAction(event){event.preventDefault();let itemActionId=event.currentTarget.dataset.actionId;this.item.system.actions[itemActionId]&&this.item.update({[`system.actions.-=${itemActionId}`]:null})}_onAddItemAttack(event){event.preventDefault();let itemAttack=new ItemAttack({name:game.i18n.localize("FLBR.ItemNewAttack")});return this.collapsibleStates[itemAttack.id]=!0,this.item.update({[`system.attacks.${itemAttack.id}`]:itemAttack.toObject()})}_onDeleteItemAttack(event){event.preventDefault();let itemAttackId=event.currentTarget.dataset.attackId;this.item.system.attacks[itemAttackId]&&this.item.update({[`system.attacks.-=${itemAttackId}`]:null})}_onAddModifier(event){event.preventDefault();let modifiers=foundry.utils.duplicate(this.item.system.modifiers??{}),modifierId=Math.max(-1,...Object.getOwnPropertyNames(modifiers))+1;return this.item.update({[`system.modifiers.${modifierId}`]:{name:"attribute.str",value:"+1"}})}_onDeleteModifier(event){event.preventDefault();let modifierId=event.currentTarget.dataset.modifierId;this.item.system.modifiers[modifierId]&&this.item.update({[`system.modifiers.-=${modifierId}`]:null})}_onAddActiveEffect(event){return event.preventDefault(),this.item.createEmbeddedDocuments("ActiveEffect",[{name:game.i18n.localize("FLBR.ACTIVE_EFFECT.New"),icon:"icons/svg/aura.svg",origin:this.item.uuid}])}_onActiveEffectAction(event){event.preventDefault();let effectId=event.currentTarget.closest("[data-effect-id]").dataset.effectId,effect=this.item.effects.get(effectId);if(effect)switch(event.currentTarget.dataset.action){case"toggle":return effect.update({disabled:!effect.disabled});case"edit":return effect.sheet.render(!0);case"delete":return effect.delete()}}_onBlastChange(event){event.preventDefault();let blast=+event.currentTarget.value;if(!(blast in FLBR.blastPowerMap)||this.item.attacks.length<=0)return;let{damage,crit}=FLBR.blastPowerMap[blast],updatedAttacks=foundry.utils.duplicate(this.item.system.attacks);for(let id in updatedAttacks)updatedAttacks[id].damage=damage,updatedAttacks[id].crit=crit;return this.item.update({"system.attacks":updatedAttacks})}_setupAccordions(){this.form?.querySelectorAll(".item-attacks-list details").forEach((el,i)=>{let id=el.dataset.actionId;(i===0&&game.settings.get(SYSTEM_ID,SETTINGS_KEYS.OPEN_FIRST_WEAPON_ATTACK)||this.collapsibleStates[id])&&(el.open=!0),el.querySelector("summary")?.addEventListener("click",()=>{this.collapsibleStates[id]=!this.collapsibleStates[id]})})}};function registerSheets(){let br=game.system.id||SYSTEM_ID;foundry.documents.collections.Actors.unregisterSheet("core",foundry.appv1.sheets.ActorSheet),foundry.documents.collections.Actors.registerSheet(br,BladeRunnerCharacterSheet,{types:[ACTOR_TYPES.CHAR],makeDefault:!0}),foundry.documents.collections.Actors.registerSheet(br,BladeRunnerVehicleSheet,{types:[ACTOR_TYPES.VEHICLE],makeDefault:!0}),foundry.documents.collections.Actors.registerSheet(br,BladeRunnerLootSheet,{types:[ACTOR_TYPES.LOOT],makeDefault:!0}),foundry.documents.collections.Items.unregisterSheet("core",foundry.appv1.sheets.ItemSheet),foundry.documents.collections.Items.registerSheet(br,BladeRunnerItemSheet,{types:[ITEM_TYPES.GENERIC],makeDefault:!0}),foundry.documents.collections.Items.registerSheet(br,BladeRunnerItemSheet,{types:[ITEM_TYPES.SYNTHETIC_AUGMENTATION],makeDefault:!0}),foundry.documents.collections.Items.registerSheet(br,BladeRunnerItemSheet,{types:[ITEM_TYPES.ARMOR],makeDefault:!0}),foundry.documents.collections.Items.registerSheet(br,BladeRunnerItemSheet,{types:[ITEM_TYPES.WEAPON],makeDefault:!0}),foundry.documents.collections.Items.registerSheet(br,BladeRunnerItemSheet,{types:[ITEM_TYPES.EXPLOSIVE],makeDefault:!0}),foundry.documents.collections.Items.registerSheet(br,BladeRunnerItemSheet,{types:[ITEM_TYPES.SPECIALTY],makeDefault:!0}),foundry.documents.collections.Items.registerSheet(br,BladeRunnerItemSheet,{types:[ITEM_TYPES.CRITICAL_INJURY],makeDefault:!0})}function preloadHandlebarsTemplates(){let paths=define_TEMPLATE_PATHS_default;return foundry.applications.handlebars.loadTemplates(paths)}function registerCustomItemTypesTemplates(){let customItemTypesTemplates=Object.values(FLBR.customItemTypesTemplates);return foundry.applications.handlebars.loadTemplates(customItemTypesTemplates)}function registerHandlebarsHelpers(){Handlebars.registerHelper("concat",function(){let str="";for(let arg in arguments)typeof arguments[arg]!="object"&&(str+=arguments[arg]);return str}),Handlebars.registerHelper("capitalize",function(str){return str.capitalize()}),Handlebars.registerHelper("toLowerCase",function(str){return str.toLowerCase()}),Handlebars.registerHelper("toUpperCase",function(str){return str.toUpperCase()}),Handlebars.registerHelper("trim",function(str,n){return n=+n,isNaN(n)&&(n=100),foundry.applications.ux.TextEditor.previewHTML(str,n)}),Handlebars.registerHelper("times",function(n,content){let str="";for(let i=0;i`${p2??foundry.utils.fromUuidSync(p1)?.name??"{undefined}"}`)),new Handlebars.SafeString(text)}),Handlebars.registerHelper("select",function(selected,options){let escapedValue=RegExp.escape(Handlebars.escapeExpression(selected)),rgx=new RegExp(` value=["']${escapedValue}["']`);return options.fn(foundry.applications.handlebars).replace(rgx,"$& selected")}),Handlebars.registerHelper("scoreSelector",function(target,options){let selectOptions=[],selected=Number(options.hash.selected);for(let[score,size]of FLBR.scoreMap){let opt=``;selectOptions.push(opt)}return new Handlebars.SafeString(``)}),Handlebars.registerHelper("boxes",function(field,options){let value=Number(options.hash.value),min=Number(options.hash.min),max=Number(options.hash.max),loss=Number(options.hash.loss),str=``;for(let i=0;i"),value>i?str+=`${FLBR.Icons.boxes.full}`:str+=`${FLBR.Icons.boxes.empty}`;if(loss<0){for(str+='';loss<0;loss++)str+=`${FLBR.Icons.boxes.lost}`;str+=""}return str+="",new Handlebars.SafeString(str)}),Handlebars.registerHelper("createNewItemButton",function(type){let title=game.i18n.format("FLBR.CreateNewItem",{type:game.i18n.localize(`TYPES.Item.${type}`)}),str=`${FLBR.Icons.buttons.plus}`;return new Handlebars.SafeString(str)})}async function initializeHandlebars(){await preloadHandlebarsTemplates(),registerHandlebarsHelpers()}function registerSystemSettings(){let sysId=game.system.id||SYSTEM_ID;game.settings.register(sysId,SETTINGS_KEYS.SYSTEM_MIGRATION_VERSION,{name:"System Migration Version",scope:"world",config:!1,type:String,default:""}),game.settings.register(sysId,SETTINGS_KEYS.DISPLAYED_MESSAGES,{name:"Displayed Messages",hint:"Used to track which messages have been displayed",scope:"world",config:!1,type:Array,default:[]}),game.settings.register(sysId,SETTINGS_KEYS.CRUSHING_TABLE,{name:"SETTINGS.BLADE_RUNNER.CrushingTableName",hint:"SETTINGS.BLADE_RUNNER.CrushingTableHint",scope:"world",config:!0,type:String,default:""}),game.settings.register(sysId,SETTINGS_KEYS.PIERCING_TABLE,{name:"SETTINGS.BLADE_RUNNER.PiercingTableName",hint:"SETTINGS.BLADE_RUNNER.PiercingTableHint",scope:"world",config:!0,type:String,default:""}),game.settings.register(sysId,SETTINGS_KEYS.CRASH_TABLE,{name:"SETTINGS.BLADE_RUNNER.CrashTableName",hint:"SETTINGS.BLADE_RUNNER.CrashTableHint",scope:"world",config:!0,type:String,default:""}),game.settings.register(sysId,SETTINGS_KEYS.EDIT_NATURE_PERMISSION,{name:"SETTINGS.BLADE_RUNNER.EditNaturePermissionName",hint:"SETTINGS.BLADE_RUNNER.EditNaturePermissionHint",scope:"world",config:!0,type:Boolean,default:!1}),game.settings.register(sysId,SETTINGS_KEYS.AUTO_APPLY_DAMAGE,{name:"SETTINGS.BLADE_RUNNER.AutomaticApplyDamageName",hint:"SETTINGS.BLADE_RUNNER.AutomaticApplyDamageHint",scope:"world",config:!0,type:Boolean,default:!0}),game.settings.register(sysId,SETTINGS_KEYS.AUTO_ARMOR_ROLL,{name:"SETTINGS.BLADE_RUNNER.AutomaticArmorRollName",hint:"SETTINGS.BLADE_RUNNER.AutomaticArmorRollHint",scope:"world",config:!0,type:Boolean,default:!1}),game.settings.register(sysId,SETTINGS_KEYS.OPEN_FIRST_WEAPON_ATTACK,{name:"SETTINGS.BLADE_RUNNER.OpenFirstWeaponAttackName",hint:"SETTINGS.BLADE_RUNNER.OpenFirstWeaponAttackHint",scope:"client",config:!0,type:Boolean,default:!0}),game.settings.register(sysId,SETTINGS_KEYS.UPDATE_ACTOR_MANEUVERABILITY_ON_CREW,{name:"SETTINGS.BLADE_RUNNER.UpdateActorManeuverabilityOnCrewName",hint:"SETTINGS.BLADE_RUNNER.UpdateActorManeuverabilityOnCrewHint",scope:"world",config:!0,type:Boolean,default:!0}),game.settings.register(sysId,SETTINGS_KEYS.UPDATE_ACTOR_MANEUVERABILITY_ON_UNCREW,{name:"SETTINGS.BLADE_RUNNER.UpdateActorManeuverabilityOnUncrewName",hint:"SETTINGS.BLADE_RUNNER.UpdateActorManeuverabilityOnUncrewHint",scope:"world",config:!0,type:Boolean,default:!1}),game.settings.register(sysId,SETTINGS_KEYS.USE_ACTIVE_EFFECTS,{name:"SETTINGS.BLADE_RUNNER.UseActiveEffectsName",hint:"SETTINGS.BLADE_RUNNER.UseActiveEffectsHint",scope:"world",config:!0,type:Boolean,default:!1}),game.settings.register(sysId,SETTINGS_KEYS.DO_NOT_USE_HANDWRITTEN_FONT,{name:"SETTINGS.BLADE_RUNNER.DoNotUseHandwrittenFontName",hint:"SETTINGS.BLADE_RUNNER.DoNotUseHandwrittenFontHint",scope:"client",config:!0,type:Boolean,default:!1,onChange:s=>changeEditorFont(s)})}function changeEditorFont(useAlternateFont){let root=document.querySelector(":root"),ff=getComputedStyle(root).getPropertyValue("--font-editor");if(useAlternateFont)ff=ff.replace('"Caveat", ',"");else if(!ff.includes("Caveat"))ff='"Caveat", '+ff;else return;root.style.setProperty("--font-editor",ff)}var ROLL_TABLE_PATTERN=/\[\[\/(?:t|table) (.+?)\]\](?:{(.+?)})?/gm;function rollTableEnricher(match,_options){let tableDoc=document.createElement("span"),table=game.tables.get(match[1]);if(table||(table=game.tables.getName(match[1])),!table)return tableDoc.innerHTML=_createBrokenLink("entity-link",match[2]??"[table?]"),tableDoc;let title=match[2]??table.name;return tableDoc.innerHTML=`${FLBR.Icons.links.rolltable}${title}`,tableDoc}var DRAW_TABLE_PATTERN=/\[\[(?:t|table): (.+?)\]\](?:{(.*?)})?(?:{(.+?)})?/gm;async function drawTableEnricher(match,options){let tableDoc=document.createElement("span"),table=game.tables.get(match[1]);if(table||(table=game.tables.getName(match[1])),!table)return tableDoc.innerHTML=_createBrokenLink("entity-link",match[2]||"[table?]"),tableDoc;let title=match[2],tableDrawOptions={displayChat:!1};match[3]&&(tableDrawOptions.roll=foundry.dice.Roll.create(match[3],options.rollData));let drawResult=await table.draw(tableDrawOptions),result=drawResult.results[0];if(!result)return tableDoc.innerHTML=_createBrokenLink("entity-link",match[2]||"[result?]"),tableDoc;let htmlFormat=` `;return result.type==="text"?htmlFormat+=(title?`${title}: `:"")+(result.name?result.name:result.description):htmlFormat+=`@UUID[${result.documentUuid}]${title?`{${title}}`:""}`,htmlFormat+="",tableDoc.innerHTML=await foundry.applications.ux.TextEditor.enrichHTML(htmlFormat,{rollData:options.rollData}),tableDoc}var CHOOSER_PATTERN=/\[\[choose: (.+?) ?#(.+?)\]\](?:{(.+?)})?/gm;async function chooserEnricher(match,options){let chooseDoc=document.createElement("span"),roll=foundry.dice.Roll.create(match[1],options.rollData);await roll.roll();let choices=match[2].split("|"),index=Math.clamp(roll.total,1,choices.length)-1,result=(match[3]?`${match[3]}: `:"")+choices[index];return chooseDoc.innerHTML=`
  • ")}
  • " +>${FLBR.Icons.links.choice}${result}
    `,chooseDoc}var ACTOR_PATTERN=/@BladeRunnerActor\[(.+?)\](?:{(.+?)})?/gm;async function actorEnricher(match,_options){let actorDoc=document.createElement("div"),actor=game.actors.get(match[1]);if(actor||(actor=game.actors.getName(match[1])),!actor)return actorDoc.innerHTML=_createBrokenLink("entity-link",match[2]||"[actor?]"),actorDoc;let title=match[2]||actor.name,sys=actor.system,attributes=[];for(let attr of FLBR.attributes){let value=sys.attributes[attr].value,text=game.i18n.localize(`FLBR.ATTRIBUTE.${attr.toUpperCase()}`)+` ${FLBR.dieMap.get(+value)}`;attributes.push(text)}let skills=[];for(let sk of FLBR.skills){let value=sys.skills[sk].value;if(value<=6)continue;let text=game.i18n.localize(`FLBR.SKILL.${sk.capitalize()}`)+` ${FLBR.dieMap.get(+value)}`;skills.push(text)}let specialties=[];for(let item of actor.items)item.type===ITEM_TYPES.SPECIALTY&&specialties.push(`@UUID[${item.uuid}]`);specialties.length===0&&specialties.push("\u2014");let gear=[];for(let item of actor.items)if(item.isPhysical){let text=(item.qty>1?`${item.qty}X `:"")+`@UUID[${item.uuid}]`;gear.push(text)}sys.signatureItem?.name&&gear.push(sys.signatureItem.name),gear.length===0&&gear.push("\u2014");let htmlFormat=`

    @UUID[${actor.uuid}]{${title}}

    +${sys.description?`
    ${sys.description}
    `:""} +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Attributes${attributes.join("
    ")}
    Health${sys.health.max}
    Resolve${sys.resolve.max}
    Skills${skills.join("
    ")}
    Specialties${specialties.join("
    ")}
    Gear${gear.join("
    ")}
    +
    +`;return actorDoc.className="flbr-enriched-actor",actorDoc.innerHTML=await foundry.applications.ux.TextEditor.enrichHTML(htmlFormat),actorDoc}var WEAPON_PATTERN=/@BladeRunnerWeapon\[(.+?)\](?:{(.+?)})?/gm;async function weaponEnricher(match,_options){let itemDoc=document.createElement("div"),item=game.items.get(match[1]);if(item||(item=game.items.getName(match[1])),!item||item.type!==ITEM_TYPES.WEAPON)return itemDoc.innerHTML=_createBrokenLink("entity-link",match[2]||"[weapon?]"),itemDoc;let title=match[2]||item.name,sys=item.system,atk=sys.attacks[item.attacks[0]?.id]||{},htmlFormat=`
    +

    @UUID[${item.uuid}]{${title}}

    + +
    ${sys.description}
    +
    +
    + + + + + + + + + + +
    DAMAGE: ${atk.damage}
    CRIT DIE: D${atk.crit}
    TYPE: ${game.i18n.localize(FLBR.damageTypes[atk.damageType])}
    MIN. RANGE: ${game.i18n.localize(FLBR.ranges[atk.range?.min])}
    MAX. RANGE: ${game.i18n.localize(FLBR.ranges[atk.range?.max])}
    AVAILABILITY: ${game.i18n.localize(FLBR.availabilities[sys.availability])}
    COST: ${sys.cost}
    +
    `;return itemDoc.className="flbr-enriched-weapon flexrow",itemDoc.style.flexWrap="nowrap",itemDoc.innerHTML=await foundry.applications.ux.TextEditor.enrichHTML(htmlFormat),itemDoc}var HANDOUT_PATTERN=/@BladeRunnerHandout\[(.+?)\](?:{(.+?)})?/gm;async function handoutEnricher(match){let divDoc=document.createElement("div"),page=await foundry.utils.fromUuid(match[1]);if(!page||page.type!=="image")return divDoc.innerHTML=_createBrokenLink("entity-link",match[2]||"[handout?]"),divDoc;let title=match[2]||page.name,htmlFormat=`

    @UUID[${page.uuid}]{${title}}

    +`;return divDoc.className="flbr-enriched-handout flbr-tab-box handout",divDoc.innerHTML=await foundry.applications.ux.TextEditor.enrichHTML(htmlFormat),divDoc}var BLADERUNNER_SYMBOL_PATTERN=/\[\[([SF]+)\]\]/gm;async function bladeRunnerSymbolEnricher(match){let symbolDoc=document.createElement("span");return symbolDoc.className="blade-runner-symbols",symbolDoc.innerHTML=match[1],symbolDoc}var FONT_AWESOME_ICON_PATTERN=/@FontAwesomeIcon\[(.+?)\]/gm;async function fontAwesomeIconEnricher(match){let iconDoc=document.createElement("i");return iconDoc.style.textIndent=0,iconDoc.className=match[1],iconDoc}var INLINE_ICON_IMAGE=/@IconImage\[(.+?)\](?:{(.+?)})?/gm;async function iconImageEnricher(match){let imgDoc=document.createElement("img");return imgDoc.setAttribute("src",match[1]),imgDoc.style.width="1em",imgDoc.style.height="1em",imgDoc.style.verticalAlign="middle",imgDoc.className="nopopout",match[2]&&imgDoc.setAttribute("data-tooltip",match[2]),imgDoc}function _createBrokenLink(type,title){return`${title}`}function enrichTextEditors(){CONFIG.TextEditor.enrichers.push({pattern:ROLL_TABLE_PATTERN,enricher:rollTableEnricher},{pattern:DRAW_TABLE_PATTERN,enricher:drawTableEnricher},{pattern:CHOOSER_PATTERN,enricher:chooserEnricher},{pattern:ACTOR_PATTERN,enricher:actorEnricher},{pattern:WEAPON_PATTERN,enricher:weaponEnricher},{pattern:HANDOUT_PATTERN,enricher:handoutEnricher},{pattern:BLADERUNNER_SYMBOL_PATTERN,enricher:bladeRunnerSymbolEnricher},{pattern:FONT_AWESOME_ICON_PATTERN,enricher:fontAwesomeIconEnricher},{pattern:INLINE_ICON_IMAGE,enricher:iconImageEnricher})}function registerDiceSoNice(dice3d){dice3d.addSystem({id:"blade-runner",name:"Blade Runner RPG"},"preferred"),dice3d.addColorset({name:"blade-runner-base-die",category:"Blade Runner RPG",description:"Blade Runner Base Die",foreground:"#fff",background:"#C53426",outline:"none",texture:"none",material:"metal",font:"IdentikalSans, Consolas, sans-Serif"},"default"),dice3d.addDicePreset({type:"d2",labels:range(2),system:"blade-runner",colorset:"blade-runner-base-die"},"d2"),dice3d.addDicePreset({type:"d3",labels:range(3),system:"blade-runner",colorset:"blade-runner-base-die"},"d3"),dice3d.addDicePreset({type:"d4",labels:range(4),system:"blade-runner",colorset:"blade-runner-base-die"},"d4"),dice3d.addDicePreset({type:"d6",labels:["systems/blade-runner/assets/dice/d6/br_d6_1.webp","2","3","4","5","systems/blade-runner/assets/dice/d6/br_d6_6.webp"],bumpMaps:["systems/blade-runner/assets/dice/d6/br_d6_1.webp",,,,,"systems/blade-runner/assets/dice/d6/br_d6_6.webp"],system:"blade-runner",colorset:"blade-runner-base-die"},"d6"),dice3d.addDicePreset({type:"d8",labels:["systems/blade-runner/assets/dice/d8/br_d8_1.webp","2","3","4","5","systems/blade-runner/assets/dice/d8/br_d8_6.webp","systems/blade-runner/assets/dice/d8/br_d8_7.webp","systems/blade-runner/assets/dice/d8/br_d8_8.webp"],bumpMaps:["systems/blade-runner/assets/dice/d8/br_d8_1_bump.webp",,,,,"systems/blade-runner/assets/dice/d8/br_d8_6_bump.webp","systems/blade-runner/assets/dice/d8/br_d8_7_bump.webp","systems/blade-runner/assets/dice/d8/br_d8_8_bump.webp"],system:"blade-runner",colorset:"blade-runner-base-die"},"d8"),dice3d.addDicePreset({type:"d10",labels:["systems/blade-runner/assets/dice/d10/br_d10_1.webp","2","3","4","5","systems/blade-runner/assets/dice/d10/br_d10_6.webp","systems/blade-runner/assets/dice/d10/br_d10_7.webp","systems/blade-runner/assets/dice/d10/br_d10_8.webp","systems/blade-runner/assets/dice/d10/br_d10_9.webp","systems/blade-runner/assets/dice/d10/br_d10_10.webp"],bumpMaps:["systems/blade-runner/assets/dice/d10/br_d10_1_bump.webp",,,,,"systems/blade-runner/assets/dice/d10/br_d10_6_bump.webp","systems/blade-runner/assets/dice/d10/br_d10_7_bump.webp","systems/blade-runner/assets/dice/d10/br_d10_8_bump.webp","systems/blade-runner/assets/dice/d10/br_d10_9_bump.webp","systems/blade-runner/assets/dice/d10/br_d10_10_bump.webp"],system:"blade-runner",colorset:"blade-runner-base-die"},"d10"),dice3d.addDicePreset({type:"d12",labels:["systems/blade-runner/assets/dice/d12/br_d12_1.webp","2","3","4","5","systems/blade-runner/assets/dice/d12/br_d12_6.webp","systems/blade-runner/assets/dice/d12/br_d12_7.webp","systems/blade-runner/assets/dice/d12/br_d12_8.webp","systems/blade-runner/assets/dice/d12/br_d12_9.webp","systems/blade-runner/assets/dice/d12/br_d12_10.webp","systems/blade-runner/assets/dice/d12/br_d12_11.webp","systems/blade-runner/assets/dice/d12/br_d12_12.webp"],bumpMaps:["systems/blade-runner/assets/dice/d12/br_d12_1_bump.webp",,,,,"systems/blade-runner/assets/dice/d12/br_d12_6_bump.webp","systems/blade-runner/assets/dice/d12/br_d12_7_bump.webp","systems/blade-runner/assets/dice/d12/br_d12_8_bump.webp","systems/blade-runner/assets/dice/d12/br_d12_9_bump.webp","systems/blade-runner/assets/dice/d12/br_d12_10_bump.webp","systems/blade-runner/assets/dice/d12/br_d12_11_bump.webp","systems/blade-runner/assets/dice/d12/br_d12_12_bump.webp"],system:"blade-runner",colorset:"blade-runner-base-die"},"d12"),dice3d.addDicePreset({type:"d20",labels:range(20),system:"blade-runner",colorset:"blade-runner-base-die"},"d20"),dice3d.addDicePreset({type:"d100",labels:range(9).map(i=>`${+i*10}`).concat("00"),system:"blade-runner",colorset:"blade-runner-base-die"},"d100")}function range(size,startAt=1){return[...Array(size).keys()].map(i=>`${i+startAt}`)}var item_piles_default={VERSION:"1.0.1",ACTOR_CLASS_TYPE:ACTOR_TYPES.LOOT,ITEM_CLASS_LOOT_TYPE:ITEM_TYPES.GENERIC,ITEM_CLASS_WEAPON_TYPE:ITEM_TYPES.WEAPON,ITEM_CLASS_EQUIPMENT_TYPE:ITEM_TYPES.ARMOR,ITEM_QUANTITY_ATTRIBUTE:"system.qty",ITEM_PRICE_ATTRIBUTE:"system.cost",ITEM_FILTERS:[{path:"type",filters:[ITEM_TYPES.SPECIALTY,ITEM_TYPES.CRITICAL_INJURY].join(",")}],UNSTACKABLE_ITEM_TYPES:[ITEM_TYPES.WEAPON,ITEM_TYPES.ARMOR,ITEM_TYPES.SYNTHETIC_AUGMENTATION],PILE_DEFAULTS:{merchantColumns:[{label:"FLBR.ItemAvailability",path:"system.availability",formatting:"{#}",buying:!0,selling:!0,mapping:{5:"FLBR.ITEM_AVAILABILITY.Incidental",4:"FLBR.ITEM_AVAILABILITY.Standard",3:"FLBR.ITEM_AVAILABILITY.Premium",2:"FLBR.ITEM_AVAILABILITY.Rare",1:"FLBR.ITEM_AVAILABILITY.Luxury"}},{label:" ",path:"system.consumable",formatting:"{#}",buying:!0,selling:!0,mapping:{true:'',false:""}},{label:'',path:"system.armor",formatting:"{#}",buying:!0,selling:!0,mapping:{0:"",6:"D",8:"C",10:"B",12:"A"}}]},ITEM_TRANSFORMER:async itemData=>(itemData?.system?.mounted&&(itemData.system.mounted=!1),itemData),ITEM_SIMILARITIES:["name","type"],CURRENCIES:[{type:"attribute",name:"FLBR.HEADER.ChinyenPoints",img:"icons/commodities/currency/coins-plain-stack-silver.webp",abbreviation:"{#}C\xA5",data:{path:"system.metaCurrencies.chinyen"},primary:!0,exchangeRate:1}],SECONDARY_CURRENCIES:[{type:"attribute",name:"FLBR.HEADER.PromotionPoints",img:"icons/commodities/treasure/medal-ribbon-gold-blue.webp",abbreviation:'{#}',data:{path:"system.metaCurrencies.promotion"}},{type:"attribute",name:"FLBR.HEADER.HumanityPoints",img:"icons/sundries/gaming/chess-knight-white.webp",abbreviation:'{#}',data:{path:"system.metaCurrencies.humanity"}}],VAULT_STYLES:[{path:"system.availability",value:AVAILABILITIES.LUXURY,styling:{"box-shadow":"inset 0px 0px 7px 0px rgba(255,119,0,1)"}},{path:"system.availability",value:AVAILABILITIES.RARE,styling:{"box-shadow":"inset 0px 0px 7px 0px rgba(255,0,247,1)"}},{path:"system.availability",value:AVAILABILITIES.PREMIUM,styling:{"box-shadow":"inset 0px 0px 7px 0px rgba(0,136,255,1)"}}]};async function getManual(){let compedium=game.packs.get("blade-runner.blade-runner-rpg-system");if(!compedium)return;let index=(await compedium.getIndex()).contents.find(i=>i.name==="Game System's Manual");return await compedium.getDocument(index._id)}var Modifier=class{constructor(key,value,item={},{active,name,type,description}={}){this.id=foundry.utils.randomID(),this.item=item,this._name=name??item.name,this._type=type??item.type,this._description=description??item.system?.description,this._key=key,this.value=+value||0;let keys=key.split(".");if(keys.length!==2)throw new SyntaxError(`Modifier "${this.name}" | Key invalid \u2192 key#length not equal to 2: "${key}"`);this.category=keys[0],this.target=keys[1],this.active=active??this.value<=0}get name(){return this._name??this.item?.name??this.target}get type(){return this._type??this.item?.type}get signedValue(){return this.value!==0&&!this.value?null:(this.value>=0?"+":"\u2212")+Math.abs(this.value)}get label(){return this.name+(this.value?` ${this.signedValue}`:"")}get description(){return(this._description??this.item.system?.description??"").replace(/<[^>]*>?/gm,"")}static getModifiers(item,path="system.modifiers",{targets=[],onlyActive=!1}={}){let out=[],mods=foundry.utils.getProperty(item,path);if(mods){for(let mod of Object.values(mods)){let m;try{m=new Modifier(mod.name,mod.value,item),out.push(m)}catch(error){ui.notifications.error(error.message,{permanent:!0})}}return Array.isArray(targets)||(targets=[targets]),(onlyActive||targets.length>0)&&(out=out.filter(m=>(onlyActive?m.active:!0)&&targets.includes(m.target))),out}}static getRangedCombatModifiers(){let category="combat";return[new Modifier(`${category}.outOfRange`,-1,{},{active:!1,name:game.i18n.localize("FLBR.RANGED_COMBAT.OutOfRange")}),new Modifier(`${category}.smallTarget`,-1,{},{active:!1,name:game.i18n.localize("FLBR.RANGED_COMBAT.SmallTarget")}),new Modifier(`${category}.largeTarget`,1,{},{active:!1,name:game.i18n.localize("FLBR.RANGED_COMBAT.LargeTarget")}),new Modifier(`${category}.targetBehindCover`,-1,{},{active:!1,name:game.i18n.localize("FLBR.RANGED_COMBAT.TargetBehindCover")}),new Modifier(`${category}.carefulAim`,1,{},{active:!1,name:game.i18n.localize("FLBR.RANGED_COMBAT.CarefulAim")}),new Modifier(`${category}.unseenTarget`,-1,{},{active:!1,name:game.i18n.localize("FLBR.RANGED_COMBAT.UnseenTarget")})]}};var CrewCollection=class extends foundry.utils.Collection{constructor(parentVehicle,sourcePropertyName,passengersPropertyName){super(),this.vehicle=parentVehicle,this._source=sourcePropertyName,this._passengers=passengersPropertyName}get source(){return foundry.utils.getProperty(this.vehicle,this._source)}get max(){return foundry.utils.getProperty(this.vehicle,this._passengers)}get full(){return this.size>=this.max}update(){this.clear();for(let occupant of this.source){let{id}=occupant,actor=game.actors.get(id);this.set(id,actor)}return this}async choose(options={}){if(this.size)return chooseActor(this.contents,{title:options.title??`${this.vehicle.name}: ${game.i18n.localize("FLBR.VEHICLE.ChoosePassenger")}`,notes:options.notes??game.i18n.localize("FLBR.VEHICLE.ChoosePassengerHint")})}};function getTable(idOrName){let table=game.tables.get(idOrName);return table||(table=game.tables.getName(idOrName)),table}var BladeRunnerActor=class extends Actor{get attributes(){return this.system.attributes}get skills(){return this.system.skills}get isVehicle(){return this.type===ACTOR_TYPES.VEHICLE}get isBroken(){switch(this.type){case ACTOR_TYPES.CHAR:for(let cap of Object.values(CAPACITIES)){let capacity=this.system[cap];if(capacity&&capacity.value<=0)return!0}return!1;case ACTOR_TYPES.VEHICLE:return this.system.hull.value<=0;default:return}}get maxPush(){switch(this.type){case ACTOR_TYPES.CHAR:return this.system.subtype===ACTOR_SUBTYPES.PC?FLBR.maxPushMap[this.system.nature]:0;default:return 1}}get armored(){switch(this.type){case ACTOR_TYPES.CHAR:return this.items.filter(i=>i.type===ITEM_TYPES.ARMOR&&i.qty>0).length;case ACTOR_TYPES.VEHICLE:return this.system.armor>0;default:return!1}}get healthBarColor(){let pct=this.system.health?.ratio;return foundry.utils.Color.fromRGB([1-pct/2,pct,0]).css}get resolveBarColor(){let pct=this.system.resolve?.ratio;return foundry.utils.Color.fromRGB([.5*pct,.7*pct,.5+pct/2]).css}get itemTypes(){let types=super.itemTypes;for(let type in types)types[type].sort((a,b)=>(a.sort||0)-(b.sort||0));return types}get rollData(){return this.getRollData()}getRollData(){let rollData=super.getRollData();if(this.type===ACTOR_TYPES.CHAR){for(let[k,v]of Object.entries(this.attributes))rollData[k]=v.value;for(let[k,v]of Object.entries(this.skills))rollData[k]=v.value}return rollData.maxPush=this.maxPush,rollData}prepareData(){switch(super.prepareData(),this.type){case ACTOR_TYPES.CHAR:this._prepareCharacterData();break;case ACTOR_TYPES.VEHICLE:this._prepareVehicleData();break}}_prepareCharacterData(){this._prepareCapacities(),this._prepareDrawSize()}_prepareCapacities(){for(let cap of Object.values(CAPACITIES)){let capacity=this.system[cap],capData=FLBR.capacitiesMap[cap];if(capacity&&capData){let natureModifier=FLBR.natureModifierMap[this.system.nature]?.[cap]??0,permanentLoss=capacity.permanentLoss??0,max=0;for(let attribute of capData.attributes)max+=this.getAttribute(attribute);max=Math.ceil(max/4)+natureModifier+capacity.mod+permanentLoss,max+=this.getRollModifiers({targets:[cap]}).reduce((tot,m)=>tot+m.value,0),max=Math.clamp(max,0,capData.max),capacity.max=max,capacity.value>max&&(capacity.value=max),capacity.ratio=capacity.value/capacity.max}}}_prepareDrawSize(){let drawSize=this.getRollModifiers({targets:"drawSize"}).reduce((tot,m)=>tot+m.value,0);drawSize>0&&(this.system.drawSize=drawSize+1)}_prepareVehicleData(){this._prepareHull(),this._prepareCrew(),this._prepareDrawSize()}_prepareHull(){let hull=this.system.hull;hull.value>hull.max&&(hull.value=hull.max,this.updateSource({"system.hull.value":hull.max}))}_prepareCrew(){if(!Object.hasOwn(this,"crew")){let c=new CrewCollection(this,"system.crew","system.passengers");Object.defineProperty(this,"crew",{value:c,writable:!1})}let updatedCrew=this.system.crew.filter(p=>game.actors.has(p.id));updatedCrew.length!==this.system.crew.length&&this.updateSource({"system.crew":updatedCrew}),this.crew.update()}async _preCreate(data,options,userId){await super._preCreate(data,options,userId);let updateData={"prototypeToken.displayName":CONST.TOKEN_DISPLAY_MODES.OWNER_HOVER};switch(this.type){case ACTOR_TYPES.CHAR:if(updateData["prototypeToken.displayBars"]=CONST.TOKEN_DISPLAY_MODES.OWNER,!this.system.attributes||!this.system.skills)throw new TypeError(`FLBR | "${this.type}" has No attribute nor skill`);if(foundry.utils.isEmpty(this.system.skills)){for(let attribute in this.system.attributes)updateData[`system.attributes.${attribute}.value`]=FLBR.startingAttributeLevel;for(let skill in FLBR.skillMap)updateData[`system.skills.${skill}.value`]=FLBR.startingSkillLevel}break;case ACTOR_TYPES.VEHICLE:updateData["prototypeToken.displayBars"]=CONST.TOKEN_DISPLAY_MODES.OWNER,updateData["prototypeToken.bar1.attribute"]="hull",this.img==="icons/svg/mystery-man.svg"&&(updateData.img=`systems/${SYSTEM_ID}/assets/icons/steering-wheel.svg`);break;case ACTOR_TYPES.LOOT:this.img==="icons/svg/mystery-man.svg"&&(updateData.img=`systems/${SYSTEM_ID}/assets/icons/cardboard-box-closed.svg`);break}foundry.utils.isEmpty(updateData)||await this.updateSource(updateData)}async addVehicleOccupant(actor){if(!this.isVehicle||this.crew.full)return;if(this.crew.has(actor.id))return ui.notifications.info("FLBR.VEHICLE.NotifPassengerAlreadyPresent",{localize:!0});let occupant={id:actor.id},crew=this.system.crew;return crew.push(occupant),await this.update({"system.crew":crew}),game.settings.get(SYSTEM_ID,SETTINGS_KEYS.UPDATE_ACTOR_MANEUVERABILITY_ON_CREW)&&await actor.updateCharacterManeuverability(this.system.maneuverability),crew}async removeVehicleOccupant(occupantId){if(!this.isVehicle)return;let crew=this.system.crew.filter(p=>p.id!==occupantId);return crew.length!==this.system.crew.length&&await this.update({"system.crew":crew}),game.settings.get(SYSTEM_ID,SETTINGS_KEYS.UPDATE_ACTOR_MANEUVERABILITY_ON_UNCREW)&&await game.actors.get(occupantId)?.updateCharacterManeuverability(0),crew}async updateCharacterManeuverability(value){if(this.type!==ACTOR_TYPES.CHAR)return;let mvr=FLBR.vehicleAttribute;this.attributes[mvr]?.value!==value&&await this.updateSource({[`system.attributes.${mvr}.value`]:value})}getRollModifiers(options){let modifiers=[];for(let i of this.items)if(i.hasModifier){let mods=i.getModifiers(options);mods.length>0&&modifiers.push(...mods)}return modifiers}getAttribute(attributeKey){return+this.attributes?.[attributeKey]?.value}getSkill(skillKey){return+this.skills?.[skillKey]?.value}async kill(){let capacity;switch(this.type){case ACTOR_TYPES.CHAR:capacity="health";break;case ACTOR_TYPES.VEHICLE:capacity="hull";break;default:return}await this.update({[`system.${capacity}.value`]:0})}async roll(rollData={},options={}){return options.title&&(rollData.title=options.title),rollData.title||(rollData.title=`${this.name}: ${game.i18n.localize("FLBR.SHEET_HEADER.GenericRoll")}`),rollData.modifiers||(rollData.modifiers=this.getRollModifiers()),new BRRollHandler({actor:this,dice:[],maxPush:this.maxPush,...rollData},{unlimitedPush:this.flags.bladerunner?.unlimitedPush,...options}).render(!0)}async rollStat(attributeKey,skillKey,options={}){if(!attributeKey)return this.roll(null,options);let attributeName=game.i18n.localize(`FLBR.ATTRIBUTE.${attributeKey.toUpperCase()}`),skillName=skillKey?game.i18n.localize(`FLBR.SKILL.${skillKey.capitalize()}`):null,title=`${this.name}: `;options.title?title=options.title:skillName?title+=`${skillName} (${attributeName})`:title+=attributeName;let attributeValue=this.getAttribute(attributeKey),skillValue=this.getSkill(skillKey),targets=[attributeKey];skillKey&&targets.push(skillKey);let dice=[];attributeValue&&dice.push(attributeValue),skillValue&&dice.push(skillValue);let modifiers=this.getRollModifiers({targets});return skillKey===SKILLS.FIREARMS&&modifiers.push(...Modifier.getRangedCombatModifiers()),new BRRollHandler({title,actor:this,attributeKey,skillKey,dice,modifiers,maxPush:this.maxPush},{unlimitedPush:this.flags.bladerunner?.unlimitedPush}).render(!0)}async rollResolve(){let title=game.i18n.localize("FLBR.ROLLER.ResolveTest");if(!await Dialog.confirm({title,content:`

    ${game.i18n.localize("FLBR.ROLLER.ResolveTestHint")}

    `}))return;if((await new BRRollHandler({title,actor:this,dice:[this.attributes.emp.value],maxPush:0}).executeRoll()).rolls[0].baneCount){ui.notifications.info(game.i18n.format("FLBR.ROLLER.ResolveTestFailed",{name:this.name}),{permanent:!0});let loss=+this.system.resolve.permanentLoss;return loss--,await this.update({"system.resolve.permanentLoss":loss}),loss}return 0}async rollRamming(){if(!this.isVehicle)return;let actor=await this.crew.choose();if(!actor)return;let driving=FLBR.vehicleSkill,hullDamage=Math.ceil(this.system.hull.max/2);return this.roll({title:`${this.name} | ${actor.name}: ${game.i18n.localize("FLBR.VEHICLE.Action.Ramming")} (${game.i18n.localize(`FLBR.SKILL.${driving.capitalize()}`)})`,actor,attributeKey:FLBR.vehicleAttribute,skillKey:driving,dice:[this.system.maneuverability,actor.skills[driving]?.value],modifiers:[...this.getRollModifiers(),...actor.getRollModifiers()],maxPush:actor.maxPush},{damage:hullDamage,damageType:DAMAGE_TYPES.CRUSHING,crit:Math.max(12,hullDamage*2),unlimitedPush:this.flags.bladerunner?.unlimitedPush||actor.flags.bladerunner?.unlimitedPush})}async applyDamage(damage,{capacity,ignoreArmor}={}){if(!capacity)switch(this.type){case ACTOR_TYPES.CHAR:capacity="health";break;case ACTOR_TYPES.VEHICLE:capacity="hull";break;default:return this}return this._applyDamage(damage,capacity,ignoreArmor)}async _applyDamage(damage,capacity,ignoreArmor=!1){if(damage<=0)return;if(!(capacity in this.system))throw new Error(`FLBR | BladeRunnerActor.applyDamage \u2192 Non-existent capacity "${capacity}"`);let initialDamage=damage;if(!ignoreArmor&&this.armored&&(damage-=await this.getArmorAblation()),damage>0){let max=this.system[capacity].max,oldVal=this.system[capacity].value,newVal=Math.clamp(oldVal-damage,0,max);newVal-oldVal!==0&&await this.update({[`system.${capacity}.value`]:newVal})}let template=`systems/${SYSTEM_ID}/templates/actor/actor-damage-chatcard.hbs`,chatData={content:await foundry.applications.handlebars.renderTemplate(template,{name:this.name,initialDamage,damage,deflectedDamage:initialDamage-damage,armored:this.armored,broken:this.isBroken,vroom:this.isVehicle,config:CONFIG.BLADE_RUNNER}),sound:CONFIG.sounds.notification,speaker:ChatMessage.getSpeaker({actor:this,token:this.token}),user:game.user.id};return ChatMessage.applyRollMode(chatData,game.settings.get("core","rollMode")),await ChatMessage.create(chatData),this}async getArmorAblation(){let armorAblation=0;if(this.type===ACTOR_TYPES.CHAR){let armors=this.itemTypes[ITEM_TYPES.ARMOR].filter(i=>i.qty>0);for(let armor of armors){let rollMessage=await armor.roll();rollMessage&&(game.dice3d&&game.dice3d.isEnabled()&&await game.dice3d.waitFor3DAnimationByMessageID(rollMessage.id),armorAblation+=rollMessage.rolls[0].successCount??0)}}else if(this.type===ACTOR_TYPES.VEHICLE){let armorRoll=game.bladerunner.roll.create(`2d${this.system.armor}p0`,{},{name:`${this.name}: ${game.i18n.localize("FLBR.ItemArmor")}`,yzur:!0});await armorRoll.roll();let armorRollMessage=await armorRoll.toMessage();game.dice3d&&game.dice3d.isEnabled()&&await game.dice3d.waitFor3DAnimationByMessageID(armorRollMessage.id),armorAblation=armorRoll.successCount}return armorAblation}async crashVehicle(massive=!1){if(!this.isVehicle)return;let formula=massive?FLBR.vehicleMassiveCrashDamage:FLBR.vehicleCrashDamage;if(!await Dialog.confirm({title:`${this.name}: ${game.i18n.localize("FLBR.VEHICLE.Action.Crash")}`,content:game.i18n.format("FLBR.VEHICLE.Action.CrashDialog",{damage:`${formula}`})}))return;let armorAblation=this.armored?await this.getArmorAblation():0,rollData=this.rollData;rollData.armor=armorAblation,formula+=" - @armor";let crashDamageRoll=foundry.dice.Roll.create(formula,rollData,{name:`${this.name}: ${game.i18n.localize("FLBR.VEHICLE.Action.Crash")}`});await crashDamageRoll.roll();let crashDamageRollMessage=await crashDamageRoll.toMessage({flavor:`${game.i18n.localize("FLBR.VEHICLE.Action.Crash")}: ${formula}`});game.dice3d&&game.dice3d.isEnabled()&&await game.dice3d.waitFor3DAnimationByMessageID(crashDamageRollMessage.id);let crashDamage=crashDamageRoll.total;if(crashDamage>0&&game.settings.get(SYSTEM_ID,SETTINGS_KEYS.AUTO_APPLY_DAMAGE))for(let passenger of this.crew){let attributeKey=ATTRIBUTES.AGILITY,skillKey=SKILLS.MOBILITY,mitigation=(await BRRollHandler.waitForRoll({title:`${this.name} | ${passenger.name}: Crash Damage Mitigation (MOBILITY)`,actor:passenger,attributeKey,skillKey,dice:[passenger.attributes[attributeKey].value,passenger.skills[skillKey].value],modifiers:[...passenger.getRollModifiers()],maxPush:0},{disabledPush:!0}).catch(err=>{}))?.successCount||0;await passenger.applyDamage(crashDamage-mitigation,{ignoreArmor:!0})}return await this.kill(),this}async explodeVehicle(){if(!this.isVehicle||!await Dialog.confirm({title:`${this.name}: ${game.i18n.localize("FLBR.VEHICLE.Action.Explode")}`,content:game.i18n.localize("FLBR.VEHICLE.Action.ExplodeHint")}))return;let blastPower=FLBR.vehicleExplosionBlastPower,blast=FLBR.blastPowerMap[FLBR.vehicleExplosionBlastPower],damageType=DAMAGE_TYPES.PIERCING,blastRoll=game.bladerunner.roll.create(`2d${blastPower}p0`,{},{damage:blast.damage,damageType,damageTypeName:game.i18n.localize(FLBR.damageTypes[damageType]),crit:blast.crit,yzur:!0});await blastRoll.roll();let blastRollMessage=await blastRoll.toMessage({flavor:game.i18n.localize("FLBR.VEHICLE.Action.Explode")});if(game.dice3d&&game.dice3d.isEnabled()&&await game.dice3d.waitFor3DAnimationByMessageID(blastRollMessage.id),blastRoll.successCount>0&&game.settings.get(SYSTEM_ID,SETTINGS_KEYS.AUTO_APPLY_DAMAGE)){let damage=blast.damage+(blastRoll.successCount-1)+1;for(let passenger of this.crew)await passenger.applyDamage(damage)}return await this.kill(),this}async drawCrit(damageType=DAMAGE_TYPES.CRUSHING,severity,critRollFormula){switch(this.type){case ACTOR_TYPES.CHAR:return this._drawCritForCharacter(damageType,severity,critRollFormula);case ACTOR_TYPES.VEHICLE:return this._drawCritForVehicle(severity,critRollFormula)}}async _drawCritForCharacter(damageType,severity,critRollFormula){if(this.type!==ACTOR_TYPES.CHAR)return;let index=damageType-DAMAGE_TYPES.CRUSHING,tableIds=[game.settings.get(SYSTEM_ID,SETTINGS_KEYS.CRUSHING_TABLE),game.settings.get(SYSTEM_ID,SETTINGS_KEYS.PIERCING_TABLE)],tables=tableIds.map(id=>getTable(id)).filter(Boolean);(!tables.length||index>tables.length-1)&&(tables=game.tables.contents);let{results}=await BladeRunnerDialog.drawTable(tables,{title:`${this.name}: ${game.i18n.localize("FLBR.CRIT.DrawCrit")}`,defaultSelected:tableIds[index],disableSelection:!game.user.isGM,disableFormula:!game.user.isGM,formula:critRollFormula,qty:severity}),itemUuid=await BladeRunnerDialog.choose(results.sort((a,b)=>b.range[0]-a.range[0]).map(r=>[r.documentUuid,`${r.name} (${r.range[0]})`]),`${this.name}: ${game.i18n.localize("FLBR.CRIT.ChooseCrit")}`,{icon:"fa-solid fa-burst"}),item=await foundry.utils.fromUuid(itemUuid),[crit]=await this.createEmbeddedDocuments("Item",[item]),template=`systems/${SYSTEM_ID}/templates/actor/actor-crit-chatcard.hbs`,resultText=await foundry.applications.ux.TextEditor.enrichHTML(game.i18n.format("FLBR.CHAT.CritInflicted",{name:`${this.name}`,crit:`@UUID[${crit.uuid}]`})),chatData={content:await foundry.applications.handlebars.renderTemplate(template,{img:crit.img,result:resultText}),speaker:ChatMessage.getSpeaker({actor:this,token:this.token,scene:canvas.scene}),user:game.user.id};return ChatMessage.applyRollMode(chatData,game.settings.get("core","rollMode")),await ChatMessage.create(chatData),crit}async _drawCritForVehicle(severity,critRollFormula){if(!this.isVehicle)return;let id=game.settings.get(SYSTEM_ID,SETTINGS_KEYS.CRASH_TABLE),table=getTable(id),tables=[];table?tables.push(table):tables=game.tables.contents;let{results}=await BladeRunnerDialog.drawTable(tables,{title:`${this.name}: ${game.i18n.localize("FLBR.CRIT.DrawVehicleDamage")}`,defaultSelected:tables[0].id,disableSelection:!game.user.isGM,disableFormula:!game.user.isGM,formula:critRollFormula,qty:severity}),resultIndex=await BladeRunnerDialog.choose(results.sort((a,b)=>b.range[0]-a.range[0]).map((r,i)=>{let title=r.description.split(":")[0];return[i,`${title} (${r.range[0]})`]}),`${this.name}: ${game.i18n.localize("FLBR.CRIT.ChooseVehicleDamage")}`,{icon:"fa-solid fa-car-burst"}),result=results[resultIndex],template=`systems/${SYSTEM_ID}/templates/actor/actor-crit-chatcard.hbs`,text=await foundry.applications.ux.TextEditor.enrichHTML(result.description),chatData={content:await foundry.applications.handlebars.renderTemplate(template,{img:result.img,result:text}),speaker:ChatMessage.getSpeaker({actor:this,token:this.token,scene:canvas.scene}),user:game.user.id};return ChatMessage.applyRollMode(chatData,game.settings.get("core","rollMode")),await ChatMessage.create(chatData),result.name}};var BladeRunnerItem=class extends Item{get qty(){return this.system.qty}get isPhysical(){return FLBR.physicalItems.includes(this.type)}get isConsumable(){return typeof this.system.consumable<"u"}get isOffensive(){return!!this.system.attacks}get melee(){if(!this.isOffensive)return!1;let atk=this.attacks[0]||{};return atk.min===atk.max===RANGES.ENGAGED}get rollable(){return!!this.system.actions}get hasAttack(){return this.isOffensive&&this.attacks.length>0}get hasAction(){return this.rollable&&this.actions.length>0}get hasModifier(){return this.system.modifiers?!foundry.utils.isEmpty(this.system.modifiers):!1}get detailedName(){let str="";return this.qty>1&&(str+=`${this.qty}x `),str+=this.name,str}get modifiersDescription(){if(!this.hasModifier)return;let out=[];for(let m of Object.values(this.system.modifiers))if(m&&m.name){let[t,n]=m.name.split("."),str="FLBR.";switch(t){case"attribute":str+=`ATTRIBUTE.${n.toUpperCase()}`;break;case"skill":str+=`SKILL.${n.capitalize()}`;break;case"capacity":str+=`HEADER.${n.capitalize()}`;break;case"combat":str+=`COMBAT.${n.capitalize()}`;break;default:str+=n.titleCase()}str=game.i18n.localize(str),t==="capacity"?str+=" "+Number(m.value)>0?"+1":"\u22121":str+=Number(m.value)>0?"+":"\u2212",out.push(str)}return out.join(", ")}static get CHAT_TEMPLATE(){return`systems/${game.system.id||SYSTEM_ID}/templates/item/item-chatcard.hbs`}prepareDerivedData(){if(this.system.actions){let itemActions=[];for(let[id,{type,name}]of Object.entries(this.system.actions))itemActions.push({id,type,name});this.actions=itemActions}if(this.type===ITEM_TYPES.WEAPON||this.type===ITEM_TYPES.EXPLOSIVE){let itemAttacks=[];for(let atk in this.system.attacks)itemAttacks.push({id:atk,name:this.system.attacks[atk].name});this.attacks=itemAttacks}}getModifiers(options={}){return Modifier.getModifiers(this,"system.modifiers",options)??[]}async consumeUnit(qty=1){return this.modifyNumberedProperty("qty",-qty)}async modifyNumberedProperty(key,mod,min=0,max=1/0){let currentValue=+foundry.utils.getProperty(this.system,key);if(typeof currentValue!="number")return;let newValue=Math.clamp(currentValue+mod,min,max);return await this.update({[`system.${key}`]:newValue}),newValue-currentValue}async roll(){if(this.system.consumable){if(this.qty<=0){ui.notifications.error(game.i18n.format("WARNING.NoItemUnit",{name:this.name}));return}let consumed=-await this.consumeUnit();ui.notifications.info(game.i18n.format("FLBR.ItemConsumeNotif",{name:this.name,qty:consumed}))}switch(this.type){case ITEM_TYPES.ARMOR:return this._rollArmor()}if(!this.rollable)return;let actor;if(this.actor?.isVehicle){if(actor=await this.actor.crew.choose(),!actor)return}else actor=this.actor;let actionId;this.actions.length>1?actionId=await BladeRunnerDialog.choose(this.actions.map(a=>{let actData=this.system.actions[a.id],itemAction=new ItemAction(actData.type,actData);return[a.id,itemAction.title]}),`${this.detailedName}: ${game.i18n.localize("FLBR.DIALOG.ChooseAction")}`):actionId=this.actions[0]?.id;let action=this.system.actions[actionId];if(!action){ui.notifications.warn("WARNING.NoItemAction",{localize:!0});return}if(action.type===ItemAction.Types.RUN_MACRO){let macro=game.macros.get(action.macro);if(macro||(macro=game.macros.getName(action.macro)),!macro){ui.notifications.warn(game.i18n.format("WARNING.ItemActionMacroNotFound",{macro:action.macro}));return}return macro.execute()}let attack;if(this.isOffensive){let attackId;this.attacks.length>1?attackId=await BladeRunnerDialog.choose(this.attacks.map(a=>{let atk=new ItemAttack(this.system.attacks[a.id]);return[a.id,atk.title]}),`${this.detailedName}: ${game.i18n.localize("FLBR.DIALOG.ChooseAttack")}`):attackId=this.attacks[0]?.id,attack=this.system.attacks[attackId]}let attributeKey=action.attribute,skillKey=action.skill,attributeName=game.i18n.localize(`FLBR.ATTRIBUTE.${attributeKey.toUpperCase()}`),skillName=skillKey?game.i18n.localize(`FLBR.SKILL.${skillKey.capitalize()}`):null,title=(this.actor?`${this.actor.name}: `:"")+`${this.detailedName} (${attributeName}${skillKey?` + ${skillName}`:""})`+(attack?` \u2192 ${attack.name}`:""),attributeValue=actor?.getAttribute(attributeKey),skillValue=actor?.getSkill(skillKey),targets=[];attributeKey&&targets.push(attributeKey),skillKey&&targets.push(skillKey);let dice=[];attributeValue&&dice.push(attributeValue),skillValue&&dice.push(skillValue);let modifiers=actor?.getRollModifiers({targets})??[];return skillKey===SKILLS.FIREARMS&&modifiers.push(...Modifier.getRangedCombatModifiers()),new BRRollHandler({title,actor,attributeKey,skillKey,dice,items:[this],modifiers,maxPush:actor?.maxPush},{damage:attack?.damage,damageType:attack?.damageType,crit:attack?.crit,unlimitedPush:actor?.flags.bladerunner?.unlimitedPush}).render(!0)}_rollArmor(){return this._rollSpecial(this.system.armor)}_rollExplosive(){return this._rollSpecial(this.system.blast)}async _rollSpecial(value){let autoArmorRoll=this.type===ITEM_TYPES.ARMOR&&game.settings.get(game.system.id,SETTINGS_KEYS.AUTO_ARMOR_ROLL),title=(this.actor?`${this.actor.name}: `:"")+game.i18n.localize(`TYPES.Item.${this.type}`);return autoArmorRoll||await Dialog.confirm({title,content:`

    ${game.i18n.format("FLBR.ROLLER.RollSpecialHint",{name:`${this.name}`})}

    `})?new BRRollHandler({title:this.name,actor:this.actor,dice:[value,value],items:[this],maxPush:0}).executeRoll():void 0}async toMessage(messageData={},{rollMode,create=!0}={}){let content=await foundry.applications.handlebars.renderTemplate(this.constructor.CHAT_TEMPLATE,{name:this.name,img:this.img,type:this.type,system:foundry.utils.duplicate(this.system),link:this.link,inActor:!!this.actor,showProperties:this.type!==ITEM_TYPES.GENERIC||this.hasModifier,isOffensive:this.isOffensive,hasAttack:this.hasAttack,hasModifier:this.hasModifier,modifiersDescription:this.modifiersDescription,config:CONFIG.BLADE_RUNNER});messageData=foundry.utils.mergeObject({user:game.user.id,speaker:ChatMessage.getSpeaker(),content,sound:CONFIG.sounds.notification},messageData);let cls=getDocumentClass("ChatMessage"),msg=new cls(messageData);return rollMode==null&&(rollMode=game.settings.get("core","rollMode")),rollMode&&msg.applyRollMode(rollMode),create?cls.create(msg):msg.toObject()}};function semverComp(min,curr,max,opt={}){if(!min&&!max||!curr)throw new Error(`Missing Comparators. min ${min}; curr ${curr}; max ${max}`);return min=min&&coerceNum(min),curr=curr&&coerceNum(curr),max=max&&coerceNum(max),min&&max&&opt.eqMin&&opt.eqMax?min===curr&&curr===max:min&&opt.eqMin?min===curr:max&&opt.eqMax?max===curr:(min?opt.gEqMin?min=min<=curr:min=minparseInt(v));if(array.some(v=>isNaN(v))||array.length!==3)throw new Error(`Invalid SemVer string: ${string}`);return array[0]=array[0]*1e6,array[1]=array[1]*1e3,array.reduce((sum,val)=>sum+=val)}async function displayMessages(){(await fetch(`systems/${SYSTEM_ID}/assets/messages/messages.jsonc`).then(resp=>resp.text()).then(jsonc=>JSON.parse(stripJSON(jsonc)))).forEach(message=>{handleDisplay(message)})}var stripJSON=data=>data.replace(/[^:]\/\/(.*)/g,""),handleDisplay=msg=>{let{content,title,type}=msg;if(isCurrent(msg)){if(type==="prompt")return displayPrompt(title,content);if(type==="chat")return sendToChat(title,content)}},isCurrent=msg=>{let isDisplayable=!msg.display==="once"||!hasDisplayed(msg.title),correctCoreVersion=foundry.utils.isNewerVersion(msg["max-core-version"]??"100.0.0",game.version)&&foundry.utils.isNewerVersion(game.version,msg["min-core-version"]??"0.0.0"),correctSysVersion=semverComp(msg["min-sys-version"]??"0.0.0",game.system.version,msg["max-sys-version"]??"100.0.0",{gEqMin:!0});return isDisplayable&&correctCoreVersion&&correctSysVersion},hasDisplayed=identifier=>!!game.settings.get(SYSTEM_ID,SETTINGS_KEYS.DISPLAYED_MESSAGES)?.includes(identifier),displayPrompt=(title,content)=>(content=content.replace("{name}",game.user.name),Dialog.prompt({title,content:`${content}`,label:"Understood!",options:{width:600,classes:[SYSTEM_ID,"dialog"]},callback:()=>setDisplayed(title)})),sendToChat=(title,content)=>{content=content.replace("{name}",game.user.name),setDisplayed(title);let footer=`
    ${game.i18n.localize("NUE.FirstLaunchHint")}
    `;return ChatMessage.create({whisper:[game.user.id],speaker:{alias:"Blade Runner RPG"},flags:{core:{canPopout:!0}},title,content:`

    ${title}

    ${content}${footer}
    `})},setDisplayed=async identifier=>{let settings=game.settings.get(SYSTEM_ID,SETTINGS_KEYS.DISPLAYED_MESSAGES)||[];settings.push(identifier),await game.settings.set(SYSTEM_ID,SETTINGS_KEYS.DISPLAYED_MESSAGES,settings.flat())};Hooks.once("init",async()=>{YearZeroRollManager.register("br",{"Roll.chatTemplate":"systems/blade-runner/templates/components/roll/roll-chatcard.hbs","Roll.tooltipTemplate":"systems/blade-runner/templates/components/roll/roll-chatcard-tooltip-partial.hbs","Roll.infosTemplate":"systems/blade-runner/templates/components/roll/roll-chatcard-infos-partial.hbs","Icons.br.base.1":"F"},{index:1}),foundry.dice.Roll.prototype.constructor.create=function(formula,data={},options={}){let n=options.yzur??("game"in data||"game"in options||"maxPush"in options||/np|p(?:\d+|@maxPush)/i.test(formula))?1:0,cls=CONFIG.Dice.rolls[n];return new cls(formula,data,options)},game.bladerunner={config:FLBR,roll:YearZeroRoll,roller:BRRollHandler,dialog:BladeRunnerDialog,macros:{rollAction,rollDice:showRollDialog,rollItem,rollStat,displayManual:async()=>(await getManual()).sheet.render(!0)},actions:new ActionCollection(FLBR.Actions.map(a=>[a.id,a]))},CONFIG.BLADE_RUNNER=FLBR,CONFIG.Actor.documentClass=BladeRunnerActor,CONFIG.Item.documentClass=BladeRunnerItem,CONFIG.Combat.initiative={formula:"1d10 + (@agi / 100)",decimals:2},registerSheets(),registerSystemSettings(),enrichTextEditors(),await initializeHandlebars(),Object.defineProperty(game,"vehicles",{enumerable:!0,get:()=>new Collection(game.actors.filter(a=>a.isVehicle).map(a=>[a.id,a]))})});Hooks.once("ready",()=>{setupMacroFolder(),Hooks.on("hotbarDrop",(_bar,data,slot)=>createBladeRunnerMacro(data,slot)),displayMessages(),registerCustomItemTypesTemplates(),Hooks.callAll("bladeRunnerReady",game.bladerunner,CONFIG.BLADE_RUNNER),game.settings.get(SYSTEM_ID,SETTINGS_KEYS.DO_NOT_USE_HANDWRITTEN_FONT)&&changeEditorFont(!0)});Hooks.once("diceSoNiceReady",dice3d=>registerDiceSoNice(dice3d));Hooks.once("yzeCombatReady",yzec=>yzec.register({actorDrawSizeAttribute:"system.drawSize"}));Hooks.once("item-piles-ready",()=>game.itempiles.API.addSystemIntegration(item_piles_default));Hooks.on("renderItemSheet",(app,_html)=>{app._element[0].style.height="auto"});Hooks.on("updateActor",(actor,updateData,_options,_userId)=>{if(foundry.utils.hasProperty(updateData,`system.${CAPACITIES.HEALTH}`)||foundry.utils.hasProperty(updateData,`system.${CAPACITIES.RESOLVE}`)||foundry.utils.hasProperty(updateData,"system.hull")){if(actor.isBroken){let statusCondition=game.i18n.localize(`FLBR.${actor.isVehicle?"Wrecked":"Broken"}`);ui.notifications.error(game.i18n.format("FLBR.SomeoneIsSomething",{name:`${actor.name}`,status:statusCondition.toLowerCase()}))}if(actor.type===ACTOR_TYPES.CHAR){let vehicles=game.vehicles.filter(v=>v.sheet?._state===Application.RENDER_STATES.RENDERED);for(let vehicle of vehicles)vehicle.crew.has(actor.id)&&vehicle.sheet.render(!0)}}});Hooks.on("deleteActor",async actor=>{if(actor.type===ACTOR_TYPES.CHAR){let vehicles=game.vehicles;for(let vehicle of vehicles)if(vehicle.crew.has(actor.id)){let crew=vehicle.system.crew.filter(p=>p.id!==actor.id);await vehicle.update({"system.crew":crew})}}});Hooks.on("getChatMessageContextOptions",addChatMessageContextOptions);Hooks.on("renderChatMessageHTML",(_msg,html,_data)=>addChatListeners(html));Hooks.on("renderChatMessageHTML",(_msg,html,_data)=>hideChatActionButtons(html));Hooks.on("chatMessage",async(_chatlog,content,_chatData)=>{let regex=/^\/([a-z]+)(?: (.+))?$/i;if(content.match(regex)){let[,command,args]=regex.exec(content);if(command==="table"||command==="t"){let table=game.tables.get(args);if(table||(table=game.tables.getName(args)),table)return await table.draw(),!1}return!0}}); 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..59de3fd --- /dev/null +++ b/lang/en.json @@ -0,0 +1,3 @@ +{ + "ZERO.SystemName": "Zero Engine" +} \ 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/packs/armors.json b/packs/armors.json new file mode 100644 index 0000000..012fea3 --- /dev/null +++ b/packs/armors.json @@ -0,0 +1,104 @@ +[ + { + "name": "Downtown Jacket", + "type": "armor", + "img": "icons/svg/shield.svg", + "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": 0, + "initiativeMod": 0 + } + }, + { + "name": "CAF Armor", + "type": "armor", + "img": "icons/svg/shield.svg", + "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": 0, + "initiativeMod": 0 + } + }, + { + "name": "Scout Armor", + "type": "armor", + "img": "icons/svg/shield.svg", + "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": 0, + "initiativeMod": 0 + } + }, + { + "name": "HARD Armor", + "type": "armor", + "img": "icons/svg/shield.svg", + "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": 0, + "initiativeMod": 0 + } + }, + { + "name": "Exo-Rig Armor", + "type": "armor", + "img": "icons/svg/shield.svg", + "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": 0, + "initiativeMod": 0 + } + }, + { + "name": "Squad Shielded Power Armor", + "type": "armor", + "img": "icons/svg/shield.svg", + "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": 0, + "initiativeMod": 0 + } + } +] 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/LOCK b/packs/blade-runner-rpg-system/LOCK new file mode 100644 index 0000000..e69de29 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..f306f3e --- /dev/null +++ b/packs/drugs.json @@ -0,0 +1,443 @@ +[ + { + "name": "Painkillers (Analgesic)", + "type": "equipment", + "img": "icons/consumables/potions/potion-bottle-corked-red.webp", + "system": { + "description": "Prescription-grade painkillers used by Medics to keep operatives moving on broken bones and bullet wounds.", + "isDrug": true, + "quantity": 1, + "cost": 0, + "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": "icons/consumables/potions/potion-flask-corked-orange.webp", + "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": 0, + "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": "icons/consumables/potions/potion-vial-corked-blue.webp", + "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": 0, + "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": "icons/consumables/potions/potion-bottle-corked-black.webp", + "system": { + "description": "Illegal DarkNight derivative of Rush; pushes the body past safe limits into true overdrive.", + "isDrug": true, + "quantity": 1, + "cost": 0, + "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": "icons/consumables/potions/potion-bottle-corked-cyan.webp", + "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": 0, + "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": "icons/consumables/potions/potion-bottle-corked-orange-red.webp", + "system": { + "description": "Standard Frother combat drug; a violent mix of stimulants, painkillers, and combat enhancers widely available on Mort.", + "isDrug": true, + "quantity": 1, + "cost": 0, + "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": "icons/consumables/potions/potion-bottle-corked-violet.webp", + "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": 0, + "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 + } + } + } +] diff --git a/packs/ebb-formulae.json b/packs/ebb-formulae.json new file mode 100644 index 0000000..42d7a3d --- /dev/null +++ b/packs/ebb-formulae.json @@ -0,0 +1,452 @@ +[ + { + "name": "Empathic Pulse", + "type": "ebb", + "img": "systems/zero-engine/assets/icons/ebb/ebb-awareness.svg", + "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/zero-engine/assets/icons/ebb/ebb-awareness.svg", + "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/zero-engine/assets/icons/ebb/ebb-awareness.svg", + "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/zero-engine/assets/icons/ebb/ebb-blast.svg", + "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/zero-engine/assets/icons/ebb/ebb-blast.svg", + "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/zero-engine/assets/icons/ebb/ebb-blast.svg", + "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/zero-engine/assets/icons/ebb/ebb-communicate.svg", + "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/zero-engine/assets/icons/ebb/ebb-communicate.svg", + "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/zero-engine/assets/icons/ebb/ebb-communicate.svg", + "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/zero-engine/assets/icons/ebb/ebb-enhance.svg", + "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/zero-engine/assets/icons/ebb/ebb-enhance.svg", + "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/zero-engine/assets/icons/ebb/ebb-enhance.svg", + "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/zero-engine/assets/icons/ebb/ebb-heal.svg", + "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/zero-engine/assets/icons/ebb/ebb-heal.svg", + "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/zero-engine/assets/icons/ebb/ebb-heal.svg", + "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/zero-engine/assets/icons/ebb/ebb-protect.svg", + "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/zero-engine/assets/icons/ebb/ebb-protect.svg", + "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/zero-engine/assets/icons/ebb/ebb-protect.svg", + "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/zero-engine/assets/icons/ebb/ebb-realityfold.svg", + "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/zero-engine/assets/icons/ebb/ebb-realityfold.svg", + "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/zero-engine/assets/icons/ebb/ebb-realityfold.svg", + "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/zero-engine/assets/icons/ebb/ebb-senses.svg", + "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/zero-engine/assets/icons/ebb/ebb-senses.svg", + "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/zero-engine/assets/icons/ebb/ebb-senses.svg", + "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/zero-engine/assets/icons/ebb/ebb-telekinesis.svg", + "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/zero-engine/assets/icons/ebb/ebb-telekinesis.svg", + "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/zero-engine/assets/icons/ebb/ebb-telekinesis.svg", + "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/zero-engine/assets/icons/ebb/ebb-thermal.svg", + "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/zero-engine/assets/icons/ebb/ebb-thermal.svg", + "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/zero-engine/assets/icons/ebb/ebb-thermal.svg", + "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/specialties.json b/packs/specialties.json new file mode 100644 index 0000000..1cb40dc --- /dev/null +++ b/packs/specialties.json @@ -0,0 +1,226 @@ +[ + { + "name": "Sniper", + "type": "specialty", + "img": "icons/svg/combat.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": "icons/svg/combat.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": "icons/svg/book.svg", + "system": { + "description": "You can’t 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": "icons/svg/book.svg", + "system": { + "description": "You have razor‑sharp 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 “Is there an immediate hidden threat here?” and receive an honest yes/no answer plus a short hint if the answer is yes." + } + }, + { + "name": "Hard Hitter", + "type": "specialty", + "img": "icons/svg/combat.svg", + "system": { + "description": "You don’t 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": "icons/svg/shield.svg", + "system": { + "description": "You simply refuse to stay down; pain and blood loss slow you, but they don’t 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": "icons/svg/mystery-man.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": "icons/svg/mystery-man.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": "icons/svg/heal.svg", + "system": { + "description": "You’re trained to work fast in filthy alleys and fire‑lit 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 → stretch, stretch → shift, shift → 2 shifts, etc., to a minimum of 1 stretch)." + } + }, + { + "name": "Healer", + "type": "specialty", + "img": "icons/svg/heal.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": "icons/svg/explosion.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": "icons/svg/explosion.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": "icons/svg/statue.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’s 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": "icons/svg/statue.svg", + "system": { + "description": "Your body is made for freezing climates; Mort’s 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": "icons/svg/statue.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": "icons/svg/statue.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 –1 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/styles/zero-engine.css b/styles/zero-engine.css new file mode 100644 index 0000000..81033e2 --- /dev/null +++ b/styles/zero-engine.css @@ -0,0 +1,4179 @@ +/* Zero Engine - SLA Industries Character Sheet */ + +/* ======================================== + SLA BORG FONTS + ======================================== */ +@font-face { + font-family: "SLA Borg Title"; + src: url("/systems/zero-engine/fonts/SLABorg-Title.woff2") format("woff2"); + font-weight: normal; + font-style: normal; +} +@font-face { + font-family: "SLA Borg UI"; + src: url("/systems/zero-engine/fonts/SLABorg-UI.woff2") format("woff2"); + font-weight: normal; + font-style: normal; +} +@font-face { + font-family: "SLA Borg Tech"; + src: url("/systems/zero-engine/fonts/SLABorg-Tech.woff2") format("woff2"); + font-weight: normal; + font-style: normal; +} +@font-face { + font-family: "SLA Borg Book"; + src: url("/systems/zero-engine/fonts/SLABorg-Book.woff2") format("woff2"); + font-weight: normal; + font-style: normal; +} + +.zero-engine { + font-family: "SLA Borg UI", "Rajdhani", "Oxanium", sans-serif; +} + +.zero-engine .window-content { + background: #1a1a1a; + color: #e0e0e0; + padding: 0; +} + +/* ======================================== + SHEET HEADER - Balanced Layout + ======================================== */ + +.zero-engine .sheet-header { + display: flex; + flex-direction: column; + padding: 0; + background: linear-gradient(135deg, #2c2c2c 0%, #1a1a1a 100%); + border-bottom: none; + gap: 0; +} + +.zero-engine .drug-alerts { + display: flex; + flex-direction: column; + gap: 6px; + padding: 8px 12px; + border-bottom: 1px solid #333; + background: rgba(0, 0, 0, 0.28); +} + +.zero-engine .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; +} + +.zero-engine .drug-alert.active { + border-left-color: #00d46d; + background: rgba(0, 130, 60, 0.18); +} + +.zero-engine .drug-alert.withdrawal { + border-left-color: #ff7a00; + background: rgba(160, 70, 0, 0.22); +} + +.zero-engine .drug-alert-main { + display: flex; + gap: 8px; + align-items: center; +} + +.zero-engine .drug-alert-title { + font-weight: 700; + color: #fff; +} + +.zero-engine .drug-alert-duration { + font-size: 11px; + color: #ddd; +} + +.zero-engine .drug-alert-meta { + flex: 1; + font-size: 11px; + color: #c8c8c8; +} + +/* --- Top Row: Avatar + Name + SCL/Credits --- */ +.zero-engine .header-top-row { + display: flex; + align-items: center; + gap: 12px; + padding: 10px 12px 8px 12px; + border-bottom: 1px solid #333; +} + +.zero-engine .header-top-row .avatar { + flex: 0 0 56px; + height: 56px; +} + +.zero-engine .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; +} + +.zero-engine .header-top-row .avatar .profile:hover { + border-color: #ff8833; +} + +.zero-engine .header-top-info { + flex: 1; + display: flex; + flex-direction: column; + gap: 4px; +} + +.zero-engine .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; +} + +.zero-engine .header-top-stats { + display: flex; + gap: 12px; +} + +.zero-engine .mini-stat { + display: flex; + align-items: center; + gap: 6px; +} + +.zero-engine .mini-stat-label { + font-size: 11px; + font-weight: bold; + color: #ff6600; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.zero-engine .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; +} + +.zero-engine .mini-stat input:focus { + border-color: #ff6600; + outline: none; +} + +/* --- Details Row: Race, Training, Archetype --- */ +.zero-engine .header-details-row { + display: grid; + grid-template-columns: 1fr 1fr 1fr 150px; + gap: 6px; + padding: 6px 12px; + border-bottom: 1px solid #333; +} + +.zero-engine .header-details-row .detail-field { + display: flex; + flex-direction: column; + gap: 2px; +} + +.zero-engine .header-details-row .detail-field label { + font-size: 9px; + font-weight: bold; + color: #ff6600; + text-transform: uppercase; + letter-spacing: 0.4px; +} + +.zero-engine .header-details-row .detail-field select, +.zero-engine .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; +} + +.zero-engine .header-details-row .detail-field select:focus, +.zero-engine .header-details-row .detail-field input[type="text"]:focus { + outline: none; + border-color: #ff6600; +} + +.zero-engine .header-details-row .armor-roll-field { + justify-content: flex-end; +} + +/* --- Vitals Row: Health, Resolve, Stress, Ebb --- */ +.zero-engine .header-vitals-row { + display: grid; + grid-template-columns: 1fr 1fr 1fr 0.7fr; + gap: 8px; + padding: 8px 12px; + border-bottom: 1px solid #333; +} + +.zero-engine .vital-box { + background: rgba(255, 102, 0, 0.08); + border: 2px solid #ff6600; + border-radius: 5px; + padding: 6px 8px; + text-align: center; +} + +.zero-engine .vital-box label { + display: block; + font-weight: bold; + color: #ff6600; + font-size: 11px; + margin-bottom: 4px; + text-transform: uppercase; + letter-spacing: 0.8px; +} + +.zero-engine .vital-values { + display: flex; + align-items: center; + justify-content: center; + gap: 4px; +} + +.zero-engine .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; +} + +.zero-engine .vital-separator { + font-size: 16px; + color: #ff6600; + font-weight: bold; +} + +/* Health box */ +.zero-engine .health-box { + border-color: #cc3333; + background: rgba(204, 51, 51, 0.08); +} + +.zero-engine .health-box label { + color: #ff4444; +} + +.zero-engine .health-box .vital-values input { + border-color: #cc3333; +} + +.zero-engine .health-box .vital-separator { + color: #cc3333; +} + +/* Resolve box */ +.zero-engine .resolve-box { + border-color: #3388cc; + background: rgba(51, 136, 204, 0.08); +} + +.zero-engine .resolve-box label { + color: #55aaff; +} + +.zero-engine .resolve-box .vital-values input { + border-color: #3388cc; +} + +.zero-engine .resolve-box .vital-separator { + color: #3388cc; +} + +/* Stress box with Panic button */ +.zero-engine .stress-vital { + border-color: #8b0000; + background: rgba(139, 0, 0, 0.12); +} + +.zero-engine .stress-vital label { + color: #ff4444; +} + +.zero-engine .stress-group { + display: flex; + align-items: center; + justify-content: center; + gap: 6px; +} + +.zero-engine .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 */ +.zero-engine .rest-vital { + border-color: #007744; + background: rgba(0, 100, 60, 0.1); + display: flex; + align-items: center; + justify-content: center; +} + +.zero-engine .rest-vital .rest-btn { + /* Override the general rest-btn defaults — fill the box */ + width: 100%; + height: 100%; + padding: 8px 12px; + font-size: 12px; +} + +.zero-engine .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; +} + +.zero-engine .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); +} + +.zero-engine .manual-panic-button i { + margin-right: 3px; +} + +.zero-engine .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; +} + +.zero-engine .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); +} + +.zero-engine .manual-armor-button i { + margin-right: 3px; +} + +.zero-engine .header-armor-roll-btn { + width: 100%; + height: 30px; + font-size: 10px; + padding: 0 6px; +} + +/* Ebb box */ +.zero-engine .ebb-vital { + border-color: #0096ff; + background: rgba(0, 150, 255, 0.08); +} + +.zero-engine .ebb-vital label { + color: #0096ff; +} + +.zero-engine .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 --- */ +.zero-engine .header-crits-row { + padding: 8px 12px 10px 12px; +} + +.zero-engine .crit-buttons { + display: flex; + gap: 8px; + margin-bottom: 8px; +} + +.zero-engine .roll-physical-crit-btn, +.zero-engine .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; +} + +.zero-engine .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); +} + +.zero-engine .roll-mental-crit-btn { + background: linear-gradient(135deg, #4b0082 0%, #2e0052 100%); + border-color: #8b5cf6; +} + +.zero-engine .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 */ +.zero-engine .active-injuries { + background: rgba(0, 0, 0, 0.25); + border-radius: 4px; + padding: 8px; + margin-top: 6px; +} + +.zero-engine .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); +} + +.zero-engine .mental-injuries h4 { + color: #8b5cf6; + border-bottom-color: rgba(139, 92, 246, 0.3); +} + +.zero-engine .injury-item { + background: rgba(255, 102, 0, 0.08); + border-left: 3px solid #ff6600; + padding: 6px 8px; + margin: 4px 0; + border-radius: 3px; +} + +.zero-engine .mental-injuries .injury-item { + background: rgba(139, 92, 246, 0.08); + border-left-color: #8b5cf6; +} + +.zero-engine .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); } +} + +.zero-engine .injury-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 6px; + margin-bottom: 2px; +} + +.zero-engine .injury-item strong { + font-size: 12px; + color: #fff; + font-weight: bold; +} + +.zero-engine .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); } +} + +.zero-engine .injury-effects { + font-size: 11px; + color: #ccc; + margin: 2px 0 0 0; + line-height: 1.3; +} + +.zero-engine .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; +} + +.zero-engine .delete-injury-btn:hover { + background: rgba(255, 0, 0, 0.35); + color: #fff; + transform: scale(1.1); +} + +/* ======================================== + SHEET TABS + ======================================== */ + +.zero-engine .sheet-tabs { + display: flex; + border-bottom: 3px solid #ff6600; + background: #2c2c2c; + gap: 0; + padding: 0; + margin: 0; +} + +.zero-engine .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; +} + +.zero-engine .sheet-tabs .item:hover { + background: rgba(255, 102, 0, 0.1); + color: #ff8833; +} + +.zero-engine .sheet-tabs .item.active { + background: rgba(255, 102, 0, 0.2); + color: #ff6600; + border-bottom-color: #ff6600; +} + +/* ======================================== + SHEET BODY + ======================================== */ + +.zero-engine .sheet-body { + padding: 8px 12px 12px 12px; + overflow-y: auto; + max-height: calc(100% - 135px); +} + +.zero-engine .sheet-body .tab { + display: none; +} + +.zero-engine .sheet-body .tab.active { + display: block; +} + +/* Section Headers */ +.zero-engine .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 + ======================================== */ + +.zero-engine .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; +} + +.zero-engine .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 */ +.zero-engine .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; +} + +.zero-engine .attr-header:hover { + background: rgba(255, 102, 0, 0.25); + transform: translateY(-1px); + box-shadow: 0 2px 8px rgba(255, 102, 0, 0.3); +} + +.zero-engine .attr-name { + font-weight: bold; + color: #ff6600; + font-size: 13px; + text-transform: uppercase; + letter-spacing: 1px; + margin-bottom: 6px; +} + +.zero-engine .attr-value-row { + display: flex; + align-items: center; + justify-content: center; + gap: 6px; + flex-wrap: wrap; +} + +.zero-engine .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; +} + +.zero-engine .attr-btn { + width: 24px; + height: 24px; + background: rgba(255, 102, 0, 0.2); + border: 2px solid #ff6600; + border-radius: 3px; + color: #ff6600; + font-size: 16px; + font-weight: bold; + cursor: pointer; + transition: all 0.2s; + display: flex; + align-items: center; + justify-content: center; + padding: 0; + line-height: 1; +} + +.zero-engine .attr-btn:hover { + background: #ff6600; + color: #000; + transform: scale(1.1); +} + +.zero-engine .attr-btn:active { + transform: scale(0.95); +} + +.zero-engine .attr-header input { + display: none; +} + +.zero-engine .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; +} + +.zero-engine .attr-roll-btn:hover { + background: #0096ff; + color: #000; + transform: scale(1.15); + box-shadow: 0 0 10px rgba(0, 150, 255, 0.6); +} + +.zero-engine .attr-roll-btn:active { + transform: scale(0.95); +} + +/* Skill Rows */ +.zero-engine .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; +} + +.zero-engine .attr-column .skill-row:hover { + background: rgba(255, 102, 0, 0.1); + border-color: #ff6600; +} + +.zero-engine .attr-column .skill-row label { + flex: 1; + color: #ccc; + font-weight: 500; + font-size: 12px; + pointer-events: none; +} + +.zero-engine .skill-controls { + display: flex; + align-items: center; + gap: 4px; +} + +.zero-engine .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; +} + +.zero-engine .skill-increment, +.zero-engine .skill-decrement { + width: 20px; + height: 20px; + background: rgba(255, 102, 0, 0.2); + border: 1px solid #ff6600; + border-radius: 3px; + color: #ff6600; + font-size: 14px; + font-weight: bold; + cursor: pointer; + transition: all 0.2s; + display: flex; + align-items: center; + justify-content: center; + padding: 0; + line-height: 1; +} + +.zero-engine .skill-increment:hover, +.zero-engine .skill-decrement:hover { + background: #ff6600; + color: #000; + transform: scale(1.15); +} + +.zero-engine .skill-increment:active, +.zero-engine .skill-decrement:active { + transform: scale(0.9); +} + +.zero-engine .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; +} + +.zero-engine .skill-roll:hover { + background: #00d4ff; + color: #000; + transform: scale(1.15); + box-shadow: 0 0 10px rgba(0, 212, 255, 0.5); +} + +.zero-engine .skill-roll:active { + transform: scale(0.95); +} + +.zero-engine .skill-row input { + display: none; +} + +/* ======================================== + SPECIALTIES, EQUIPMENT, BIOGRAPHY + ======================================== */ + +.zero-engine .specialties-section { + margin-top: 20px; + padding: 15px; + background: rgba(255, 102, 0, 0.05); + border: 2px solid #ff6600; + border-radius: 8px; +} + +.zero-engine .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 */ +.zero-engine .specialty-list { + display: flex; + flex-direction: column; + gap: 6px; +} + +.zero-engine .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; +} + +.zero-engine .specialty-item:hover { + background: rgba(255, 102, 0, 0.1); + border-color: #ff6600; +} + +.zero-engine .specialty-img { + flex-shrink: 0; + border-radius: 4px; + border: 1px solid #555; +} + +.zero-engine .specialty-info { + flex: 1; + min-width: 0; +} + +.zero-engine .specialty-name-row { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 2px; +} + +.zero-engine .specialty-name { + font-weight: bold; + font-size: 13px; + color: #fff; + cursor: pointer; +} + +.zero-engine .specialty-name:hover { + color: #ff6600; + text-decoration: underline; +} + +.zero-engine .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; +} + +.zero-engine .specialty-effects { + font-size: 11px; + color: #aaa; + line-height: 1.3; +} + +.zero-engine .specialty-empty { + text-align: center; + padding: 15px; + color: #666; + font-size: 12px; +} + +.zero-engine .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; +} + +.zero-engine .delete-specialty-btn:hover { + background: rgba(255, 0, 0, 0.35); + color: #fff; + transform: scale(1.1); +} + +.zero-engine .specialties-section, +.zero-engine .equipment-section, +.zero-engine .biography-section, +.zero-engine .gear-section, +.zero-engine .bio-section, +.zero-engine .weapons-section { + margin-bottom: 20px; +} + +.zero-engine .specialties-section h2, +.zero-engine .equipment-section h2, +.zero-engine .biography-section h2, +.zero-engine .gear-section h2, +.zero-engine .bio-section h2, +.zero-engine .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; +} + +.zero-engine .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; +} + +.zero-engine .editor { + min-height: 150px; + background: rgba(255, 255, 255, 0.05); + border: 1px solid #333; + border-radius: 6px; + padding: 10px; + color: #e0e0e0; +} + +.zero-engine .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; +} + +.zero-engine .editor textarea:focus { + outline: none; + border-color: #ff6600; + box-shadow: 0 0 8px rgba(255, 102, 0, 0.3); +} + +.zero-engine .editor textarea::placeholder { + color: #666; + font-style: italic; +} + +/* ======================================== + FORM INPUTS + ======================================== */ + +.zero-engine input:focus { + outline: none; + border-color: #ff8833; + box-shadow: 0 0 8px rgba(255, 102, 0, 0.3); +} + +.zero-engine input::placeholder { + color: #666; +} + +/* ======================================== + SCROLLBAR + ======================================== */ + +.zero-engine .sheet-body::-webkit-scrollbar { + width: 8px; +} + +.zero-engine .sheet-body::-webkit-scrollbar-track { + background: #1a1a1a; +} + +.zero-engine .sheet-body::-webkit-scrollbar-thumb { + background: #ff6600; + border-radius: 4px; +} + +.zero-engine .sheet-body::-webkit-scrollbar-thumb:hover { + background: #ff8833; +} + +/* ======================================== + YZE DICE DIALOG + ======================================== */ + +.zero-engine.dialog .yze-roll-dialog { + padding: 15px; + background: #1a1a1a; +} + +.zero-engine.dialog .yze-roll-dialog .form-group { + margin-bottom: 18px; +} + +.zero-engine.dialog .yze-roll-dialog label { + display: block; + font-weight: bold; + color: #ff8833; + margin-bottom: 8px; + font-size: 15px; + text-transform: uppercase; + letter-spacing: 1px; +} + +.zero-engine.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; +} + +.zero-engine.dialog .modify-dice:hover { + background: #ff6600; + color: #000; + transform: scale(1.05); + box-shadow: 0 0 10px rgba(255, 102, 0, 0.6); +} + +.zero-engine.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; +} + +.zero-engine.dialog input[type="number"]:focus { + border-color: #ff6600; + outline: none; + box-shadow: 0 0 8px rgba(255, 102, 0, 0.4); +} + +.zero-engine.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; +} + +.zero-engine.dialog select:focus { + border-color: #ff6600; + outline: none; + box-shadow: 0 0 6px rgba(255, 102, 0, 0.4); +} + +.zero-engine.dialog select option { + background: #1a1a1a; + color: #e0e0e0; +} + +/* Dialog footer buttons (Roll / Cancel) */ +.zero-engine.dialog .dialog-button, +.zero-engine.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; +} +.zero-engine.dialog .dialog-button:hover, +.zero-engine.dialog footer button:hover { + background: rgba(255, 102, 0, 0.25); + border-color: #ff6600; + color: #fff; +} +/* Primary action (Roll) */ +.zero-engine.dialog .dialog-button.default, +.zero-engine.dialog footer button.default, +.zero-engine.dialog .dialog-button[data-button="roll"] { + background: rgba(255, 102, 0, 0.2); + border-color: #ff6600; + color: #ff8833; +} +.zero-engine.dialog .dialog-button.default:hover, +.zero-engine.dialog footer button.default:hover, +.zero-engine.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: rgba(139, 0, 0, 0.2); + border: 2px solid #8b0000; + border-radius: 6px; + padding: 15px; + margin: 10px 0; +} + +.panic-formula { + font-size: 14px; + color: #e0e0e0; + margin-bottom: 10px; +} + +.panic-total { + font-size: 20px; + color: #ff4444; + font-weight: bold; +} + +.panic-success { + background: rgba(0, 128, 0, 0.2); + border: 2px solid #00aa00; + border-radius: 4px; + padding: 12px; + color: #00ff00; + font-weight: bold; + text-align: center; + margin-top: 10px; + font-size: 16px; +} + +.panic-effect { + background: rgba(0, 0, 0, 0.4); + border: 1px solid #ff4444; + border-radius: 4px; + padding: 12px; + margin-top: 10px; +} + +.panic-effect strong { + color: #ff6b6b; + font-size: 16px; + display: block; + margin-bottom: 8px; + text-transform: uppercase; + letter-spacing: 1px; +} + +.critical-injury { + background: rgba(220, 20, 60, 0.2); + border: 2px solid #dc143c; + border-radius: 6px; + padding: 15px; + margin: 10px 0; +} + +.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: rgba(255, 170, 0, 0.2); + border: 1px solid #ffaa00; + border-radius: 4px; + padding: 8px; + color: #ffaa00; + font-weight: bold; + margin: 8px 0; +} + +.crit-effects, +.crit-healing { + background: rgba(0, 0, 0, 0.3); + border-radius: 4px; + padding: 8px; + margin: 8px 0; +} + +.crit-effects strong, +.crit-healing strong { + color: #ff6600; + display: block; + margin-bottom: 4px; +} + +/* ======================================== + ROLLABLE INDICATOR + ======================================== */ + +.zero-engine .rollable { + position: relative; +} + +.zero-engine .rollable::after { + content: '\1F3B2'; + position: absolute; + top: 5px; + right: 5px; + font-size: 12px; + opacity: 0; + transition: opacity 0.2s; +} + +.zero-engine .rollable:hover::after { + opacity: 0.6; +} + +/* ======================================== + ITEM SHEET STYLES + ======================================== */ + +.zero-engine.sheet.item .window-content { + background: #1a1a1a; + color: #e0e0e0; + padding: 0; +} + +.zero-engine.sheet.item .sheet-header { + background: linear-gradient(135deg, #2c2c2c 0%, #1a1a1a 100%); + padding: 10px 12px; + border-bottom: 2px solid #ff6600; +} + +.zero-engine.sheet.item .item-header-row { + display: flex; + align-items: center; + gap: 12px; +} + +.zero-engine.sheet.item .item-photo { + flex: 0 0 64px; + height: 64px; +} + +.zero-engine.sheet.item .item-photo img { + width: 100%; + height: 100%; + border: 2px solid #ff6600; + border-radius: 6px; + cursor: pointer; + object-fit: cover; +} + +.zero-engine.sheet.item .item-header-info { + flex: 1; +} + +.zero-engine.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; +} + +.zero-engine.sheet.item .item-name input:focus { + outline: none; + border-bottom-color: #ff8833; +} + +.zero-engine.sheet.item .item-type-label { + font-size: 11px; + color: #ff6600; + text-transform: uppercase; + letter-spacing: 1px; + margin-top: 4px; +} + +.zero-engine.sheet.item .sheet-body { + padding: 12px; + overflow-y: auto; + max-height: calc(100% - 90px); +} + +/* Specialty Sheet Content */ +.zero-engine .specialty-sheet-content { + display: flex; + flex-direction: column; + gap: 12px; +} + +.zero-engine .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; +} + +.zero-engine .specialty-meta-row { + display: flex; + align-items: center; + gap: 10px; +} + +.zero-engine .specialty-meta-row label { + flex: 0 0 140px; + font-size: 11px; + font-weight: bold; + color: #ff6600; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.zero-engine .specialty-meta-row select, +.zero-engine .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; +} + +.zero-engine .specialty-meta-row select:focus, +.zero-engine .specialty-meta-row input[type="text"]:focus { + outline: none; + border-color: #ff6600; +} + +.zero-engine .specialty-description { + flex: 1; +} + +.zero-engine .specialty-description > label { + display: block; + font-size: 11px; + font-weight: bold; + color: #ff6600; + text-transform: uppercase; + letter-spacing: 0.5px; + margin-bottom: 6px; +} + +.zero-engine .specialty-description .editor { + min-height: 200px; +} + +/* Item Props (Weapon/Armor/Equipment) */ +.zero-engine .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; +} + +.zero-engine .item-prop { + display: flex; + flex-direction: column; + gap: 4px; +} + +.zero-engine .item-prop label { + font-size: 10px; + font-weight: bold; + color: #ff6600; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.zero-engine .item-prop input, +.zero-engine .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; +} + +.zero-engine .item-prop input:focus, +.zero-engine .item-prop select:focus { + outline: none; + border-color: #ff6600; +} + +.zero-engine .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 + ======================================== */ + +.zero-engine .weapon-list { + display: flex; + flex-direction: column; + gap: 4px; +} + +.zero-engine .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; +} + +.zero-engine .weapon-item:hover { + background: rgba(255, 102, 0, 0.1); + border-color: #ff6600; +} + +.zero-engine .weapon-img { + flex-shrink: 0; + border-radius: 4px; + border: 1px solid #555; +} + +.zero-engine .weapon-info { + flex: 1; + min-width: 0; +} + +.zero-engine .weapon-name-row { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 2px; +} + +.zero-engine .weapon-name { + font-weight: bold; + font-size: 13px; + color: #fff; + cursor: pointer; +} + +.zero-engine .weapon-name:hover { + color: #ff6600; + text-decoration: underline; +} + +.zero-engine .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; +} + +.zero-engine .weapon-type-badge.ranged { + color: #00d4ff; + background: rgba(0, 212, 255, 0.15); + border: 1px solid rgba(0, 212, 255, 0.3); +} + +.zero-engine .weapon-type-badge.melee { + color: #ff4444; + background: rgba(255, 68, 68, 0.15); + border: 1px solid rgba(255, 68, 68, 0.3); +} + +.zero-engine .weapon-stats-row { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.zero-engine .weapon-stat { + font-size: 11px; + color: #aaa; +} + +.zero-engine .weapon-stat strong { + color: #ff6600; + font-size: 10px; + margin-right: 2px; +} + +.zero-engine .weapon-actions { + display: flex; + flex-direction: row; + align-items: center; + gap: 6px; + flex-shrink: 0; +} + +.zero-engine .weapon-aux-actions { + display: flex; + flex-direction: column; + gap: 3px; +} + +.zero-engine .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; +} + +.zero-engine .weapon-roll-btn i { + font-size: 13px; + flex-shrink: 0; +} + +.zero-engine .weapon-roll-btn:hover { + background: #00d4ff; + color: #000; + box-shadow: 0 0 14px rgba(0, 212, 255, 0.55); +} + +.zero-engine .weapon-roll-btn:active { + transform: scale(0.95); +} + +.zero-engine .weapon-roll-btn.btn-locked, +.zero-engine .weapon-roll-btn:disabled { + background: rgba(80,80,80,0.2); + border-color: #555; + color: #555; + cursor: not-allowed; + box-shadow: none; +} + +.zero-engine .weapon-item.weapon-ammo-empty { + border-left-color: #cc4400; + background: rgba(80,20,0,0.2); + opacity: 0.8; +} + +.zero-engine .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; +} + +.zero-engine .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; +} + +.zero-engine .toggle-equip-btn:hover { + border-color: #cfcfcf; + color: #fff; + transform: translateY(-1px); +} + +.zero-engine .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); +} + +.zero-engine .drug-activate-btn, +.zero-engine .drug-off-btn, +.zero-engine .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; +} + +.zero-engine .drug-alert .drug-off-btn { + width: auto; + min-width: 54px; + padding: 0 8px; + font-size: 11px; +} + +.zero-engine .drug-activate-btn { + border-color: #26c96b; + color: #9fffc4; + background: rgba(0, 130, 60, 0.2); +} + +.zero-engine .drug-off-btn { + border-color: #ff9f40; + color: #ffd6a9; + background: rgba(170, 80, 0, 0.22); +} + +.zero-engine .drug-clear-withdrawal-btn { + border-color: #9db4cc; + color: #d7e7ff; + background: rgba(40, 70, 110, 0.22); +} + +.zero-engine .drug-activate-btn:hover, +.zero-engine .drug-off-btn:hover, +.zero-engine .drug-clear-withdrawal-btn:hover { + transform: translateY(-1px); + filter: brightness(1.2); +} + +.zero-engine .drug-item.drug-active { + border-left-color: #00d46d; + background: rgba(0, 110, 55, 0.16); +} + +.zero-engine .drug-item.drug-withdrawal { + border-left-color: #ff7a00; + background: rgba(150, 75, 0, 0.2); +} + +.zero-engine .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; +} + +.zero-engine .armor-roll-btn:hover { + background: #cfa35a; + color: #111; + transform: scale(1.1); + box-shadow: 0 0 10px rgba(207, 163, 90, 0.45); +} + +.zero-engine .armor-roll-btn:active { + transform: scale(0.95); +} + +.zero-engine .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; +} + +.zero-engine .delete-weapon-btn:hover { + background: rgba(255, 0, 0, 0.35); + color: #fff; + transform: scale(1.1); +} + +.zero-engine .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; +} + +.zero-engine .delete-armor-btn:hover { + background: rgba(255, 0, 0, 0.35); + color: #fff; + transform: scale(1.1); +} + +.zero-engine .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; +} + +.zero-engine .delete-gear-btn:hover { + background: rgba(255, 0, 0, 0.35); + color: #fff; + transform: scale(1.1); +} + +.zero-engine .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; +} + +.zero-engine .delete-drug-btn:hover { + background: rgba(255, 0, 0, 0.35); + color: #fff; + transform: scale(1.1); +} + +.zero-engine .weapon-empty { + text-align: center; + padding: 12px; + color: #666; + font-size: 12px; +} + +/* REST button */ +.zero-engine .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; +} + +.zero-engine .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 */ +.zero-engine .fin-amount { + display: flex; + align-items: center; + gap: 4px; +} + +.zero-engine .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) */ +.zero-engine .init-buttons-inline { + display: flex; + gap: 6px; + align-items: center; + margin-left: auto; +} + +.zero-engine .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; +} + +.zero-engine .init-buttons-inline button:hover { + background: #3a3a3a; + color: #ffd56a; +} + +.zero-engine .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; Zero Engine 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 */ +.zero-engine .section-header, +.zero-engine h2, +.zero-engine h3, +.zero-engine h4, +.zero-engine .sheet-tab-nav .item, +.zero-engine .tab-label, +.zero-engine .drug-name, +.zero-engine .specialties-section h2, +.zero-engine .equipment-section h2, +.zero-engine .biography-section h2, +.zero-engine .gear-section h2, +.zero-engine .bio-section h2, +.zero-engine .weapons-section h2 { + font-family: "SLA Borg Title", "Orbitron", "Rajdhani", sans-serif; +} + +/* Technical / numeric values: SLA Borg Tech */ +.zero-engine .boxed-value, +.zero-engine .health-box, +.zero-engine .resolve-box, +.zero-engine .ze-attr-die, +.zero-engine .ze-skill-die, +.zero-engine .stress-value, +.zero-engine .scl-value, +.zero-engine .credits-value, +.zero-engine input[type="number"] { + font-family: "SLA Borg Tech", "Share Tech Mono", "Courier New", monospace; +} + +/* Actor name: SLA Borg Title, larger */ +.zero-engine .actor-name input { + font-family: "SLA Borg Title", "Orbitron", sans-serif; +} + +/* ======================================== + VERTICAL SKILLS LAYOUT (.ze-stats) + ======================================== */ + +.zero-engine .ze-stats { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 12px; + align-items: start; + padding: 12px 10px; +} + +/* One block per attribute */ +.zero-engine .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 */ +.zero-engine .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); +} + +.zero-engine .ze-attr-roll-btn { + display: flex; + align-items: center; + gap: 6px; + background: none; + border: none; + cursor: pointer; + padding: 0; + flex: 1; +} + +.zero-engine .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; +} + +.zero-engine .ze-attr-roll-btn i { + color: #ff6600; + font-size: 13px; + opacity: 0.7; +} + +.zero-engine .ze-attr-roll-btn:hover .ze-attr-name { + color: #ffaa55; +} +.zero-engine .ze-attr-roll-btn:hover i { + opacity: 1; +} + +/* Score selector (dots/boxes) for attribute */ +.zero-engine .ze-attr-score { + display: flex; + align-items: center; +} + +/* Attribute die value badge */ +.zero-engine .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; +} +.zero-engine .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 */ +.zero-engine .ze-skills-list { + display: flex; + flex-direction: column; + gap: 0; +} + +/* Individual skill row */ +.zero-engine .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; +} + +.zero-engine .ze-skill-row:last-child { + border-bottom: none; +} + +.zero-engine .ze-skill-row:hover { + background: rgba(255, 102, 0, 0.08); +} + +/* Skill name link */ +.zero-engine .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; +} + +.zero-engine .ze-skill-name:hover { + color: #ff8833; + text-decoration: none; +} + +/* Score selector for skill */ +.zero-engine .ze-skill-score { + display: flex; + align-items: center; +} + +/* Skill die value badge */ +.zero-engine .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; +} + +.zero-engine .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 */ +.zero-engine .flux-vital { + border-color: #00cccc !important; +} +.zero-engine .flux-vital label { + color: #00cccc !important; + font-family: "SLA Borg Title", "Orbitron", sans-serif; + letter-spacing: 1.5px; +} +.zero-engine .flux-vital .vital-values { + display: flex; + align-items: center; + gap: 4px; +} +.zero-engine .flux-vital .flux-current { + color: #00cccc !important; + border-color: #00cccc !important; + background: rgba(0, 204, 204, 0.08) !important; + width: 50px; + text-align: center; +} +.zero-engine .flux-vital .vital-separator { + color: #888; + font-weight: bold; +} +.zero-engine .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 */ +.zero-engine .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; +} +.zero-engine .ebb-skill-row label { + color: #00cccc !important; + font-weight: bold; +} + +/* Ebb Formulae section */ +.zero-engine .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; +} +.zero-engine .ebb-formulae-section .section-header i { + color: #00cccc; +} +.zero-engine .flux-indicator { + font-size: 12px; + color: #aaa; + font-family: "SLA Borg UI", sans-serif; + font-weight: normal; + letter-spacing: 0; + text-transform: none; +} +.zero-engine .flux-indicator strong { + color: #00cccc; +} +.zero-engine .ebb-discipline-badge { + background: rgba(0, 100, 150, 0.5) !important; + border-color: #00aacc !important; + color: #00cccc !important; +} +.zero-engine .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 */ +.zero-engine .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; +} +.zero-engine .ebb-formula-roll-btn { + background: rgba(0, 100, 150, 0.4); + border: 1px solid #00aacc !important; + color: #00cccc !important; +} +.zero-engine .ebb-formula-roll-btn:hover { + background: rgba(0, 150, 200, 0.5) !important; + color: #fff !important; +} +.zero-engine .ebb-formula-edit-btn { + border-color: #555 !important; + color: #999 !important; +} +.zero-engine .ebb-formula-edit-btn:hover { + border-color: #aaa !important; + color: #fff !important; + background: rgba(255,255,255,0.1) !important; +} +.zero-engine .ebb-formula-delete-btn { + border-color: #662222 !important; + color: #cc4444 !important; + background: rgba(100, 0, 0, 0.2) !important; +} +.zero-engine .ebb-formula-delete-btn:hover { + border-color: #ff4444 !important; + color: #fff !important; + background: rgba(180, 0, 0, 0.35) !important; +} + +/* Flux control buttons */ +.zero-engine .ebb-flux-controls { + display: flex; + gap: 8px; + padding: 8px 12px; + border-top: 1px solid rgba(0, 170, 204, 0.2); + margin-top: 4px; +} +.zero-engine .ebb-flux-recover-btn, +.zero-engine .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; +} +.zero-engine .ebb-flux-recover-btn:hover, +.zero-engine .ebb-flux-full-btn:hover { + background: rgba(0, 130, 160, 0.5); + color: #fff; +} + +/* Ebb item sheet */ +.zero-engine .ebb-sheet-content .item-prop-full { + padding: 6px 12px; +} +.zero-engine .ebb-sheet-content .item-prop-full label { + display: block; + color: #888; + font-size: 11px; + text-transform: uppercase; + letter-spacing: 1px; + margin-bottom: 4px; +} +.zero-engine .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; +} +.zero-engine .ebb-sheet-content textarea:focus { + border-color: #00aacc; + outline: none; +} + +/* ═══════════════════════════════════════════════════════════ + EBB TAB LAYOUT + ═══════════════════════════════════════════════════════════ */ + +/* Tab header: flux tracker + skill side by side */ +.zero-engine .ebb-tab-header { + display: flex; + gap: 12px; + padding: 10px 10px 0; + margin-bottom: 12px; + flex-wrap: wrap; +} + +/* Flux status panel */ +.zero-engine .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; +} +.zero-engine .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; +} +.zero-engine .ebb-flux-track { + display: flex; + align-items: center; + gap: 4px; +} +.zero-engine .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; +} +.zero-engine .ebb-flux-input::-webkit-inner-spin-button, +.zero-engine .ebb-flux-input::-webkit-outer-spin-button { -webkit-appearance: none; } +.zero-engine .ebb-flux-sep { + color: #666; + font-size: 18px; +} +.zero-engine .ebb-flux-max { + font-size: 22px; + color: #555; + font-weight: bold; +} +.zero-engine .ebb-flux-actions { + display: flex; + gap: 6px; + margin-left: auto; +} + +/* Ebb skill panel */ +.zero-engine .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; +} +.zero-engine .ebb-skill-label { + color: #7799cc; + font-family: "SLA Borg Title", "Orbitron", sans-serif; + font-size: 13px; + letter-spacing: 1.5px; + text-transform: uppercase; +} +.zero-engine .ebb-skill-controls { + display: flex; + align-items: center; + gap: 6px; +} +.zero-engine .ebb-skill-value { + font-size: 20px !important; +} +.zero-engine .ebb-tab-skill-roll { + padding: 4px 10px; + height: auto !important; + width: auto !important; + font-size: 13px !important; + gap: 4px; + display: flex; + align-items: center; +} +.zero-engine .ebb-dice-summary { + color: #556688; + font-size: 11px; + font-style: italic; +} + +/* Formulae section within the tab */ +.zero-engine .tab[data-tab="ebb"] .ebb-formulae-section { + padding: 0 10px 10px; +} +.zero-engine .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; +} +.zero-engine .ebb-formula-count { + margin-left: auto; + font-size: 11px; + color: #556; + font-style: italic; +} + +/* Discipline badge colours on the Ebb tab */ +.zero-engine .ebb-disc-awareness { background: rgba(100, 180, 255, 0.25); color: #64b4ff; border: 1px solid #64b4ff; } +.zero-engine .ebb-disc-blast { background: rgba(255, 102, 0, 0.25); color: #ff6600; border: 1px solid #ff6600; } +.zero-engine .ebb-disc-communicate { background: rgba(180, 100, 255, 0.25); color: #b464ff; border: 1px solid #b464ff; } +.zero-engine .ebb-disc-enhance { background: rgba( 80, 220, 120, 0.25); color: #50dc78; border: 1px solid #50dc78; } +.zero-engine .ebb-disc-heal { background: rgba( 80, 220, 180, 0.25); color: #50dcb4; border: 1px solid #50dcb4; } +.zero-engine .ebb-disc-protect { background: rgba(220, 200, 80, 0.25); color: #dcc850; border: 1px solid #dcc850; } +.zero-engine .ebb-disc-realityFold { background: rgba(220, 80, 220, 0.25); color: #dc50dc; border: 1px solid #dc50dc; } +.zero-engine .ebb-disc-senses { background: rgba(255, 220, 80, 0.25); color: #ffdc50; border: 1px solid #ffdc50; } +.zero-engine .ebb-disc-telekinesis { background: rgba( 80, 180, 255, 0.25); color: #50b4ff; border: 1px solid #50b4ff; } +.zero-engine .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 */ +.zero-engine .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); +} +.zero-engine .ebb-flux-cost { + color: #00cccc !important; +} +.zero-engine .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 ───────────────────────────── */ +.zero-engine .armor-name-display { font-size: 12px; margin-bottom: 3px; } +.zero-engine .equipped-armor-name { color: #00d4ff; font-weight: bold; text-transform: uppercase; letter-spacing: 0.5px; } +.zero-engine .equipped-armor-name.none { color: #555; font-style: italic; text-transform: none; } +.zero-engine .finances-section { padding: 12px; } +.zero-engine .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; } +.zero-engine .fin-credit-label { font-size: 11px; color: #888; text-transform: uppercase; letter-spacing: 1px; } +.zero-engine .fin-credit-value { font-size: 28px; font-weight: bold; color: #00d4ff; } +.zero-engine .fin-credit-value input { background: transparent; border: none; color: #00d4ff; font-size: 26px; font-weight: bold; width: 100px; } +.zero-engine .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; } +.zero-engine .finances-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-bottom: 12px; } +.zero-engine .fin-panel { background: rgba(0,0,0,0.25); border: 1px solid rgba(255,102,0,0.2); border-radius: 6px; padding: 10px; } +.zero-engine .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); } +.zero-engine .income-panel .fin-panel-title { color: #44cc88; } +.zero-engine .expense-panel .fin-panel-title { color: #ff6644; } +.zero-engine .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; } +.zero-engine .fin-row label { color: #aaa; } +.zero-engine .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; } +.zero-engine .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; } +.zero-engine .fin-total-val { font-size: 15px; color: #ff9955; } +.zero-engine .income-total .fin-total-val { color: #44cc88; } +.zero-engine .expense-total .fin-total-val { color: #ff6644; } +.zero-engine .fin-btax { color: #ff9900; font-weight: bold; } +.zero-engine .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; } +.zero-engine .fin-net-label { font-size: 12px; text-transform: uppercase; letter-spacing: 1px; color: #888; } +.zero-engine .fin-net-val { font-size: 22px; font-weight: bold; } +.zero-engine .net-positive { color: #44cc88; } .zero-engine .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 ── */ +.zero-engine .weapons-section:has(h2 .fa-toolbox) h2, +.zero-engine .gear-section-header { + background: linear-gradient(90deg, #aa7700 0%, transparent 100%) !important; +} + +/* ── Drugs section: magenta/purple accent ── */ +.zero-engine .weapons-section:has(h2 .fa-pills) h2, +.zero-engine .drugs-section-header { + background: linear-gradient(90deg, #880077 0%, transparent 100%) !important; +} + +/* Drug item — richer status display */ +.zero-engine .drug-item { + position: relative; +} + +.zero-engine .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); +} + +.zero-engine .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); +} + +/* ── Active Conditions Row ──────────────────────────────── */ +.zero-engine .active-conditions-row { + display: flex; flex-wrap: wrap; gap: 5px; margin-top: 6px; padding: 5px 0; +} +.zero-engine .condition-tag { + display: flex; align-items: center; gap: 4px; + background: rgba(200,80,0,0.15); border: 1px solid rgba(200,100,0,0.4); + border-radius: 3px; padding: 3px 7px; font-size: 11px; color: #e0a060; + font-family: "SLA Borg UI", sans-serif; text-transform: uppercase; letter-spacing: 0.5px; +} +.zero-engine .condition-tag.sla-bleeding { border-color: #aa2222; background: rgba(160,20,20,0.2); color: #ff6666; } +.zero-engine .condition-tag.sla-stunned { border-color: #aaaa22; background: rgba(140,140,20,0.18); color: #ffff88; } +.zero-engine .condition-tag.sla-winded { border-color: #888822; background: rgba(120,120,20,0.15); color: #dddd66; } +.zero-engine .condition-tag.sla-broken-arm,.zero-engine .condition-tag.sla-broken-leg { border-color: #882200; background: rgba(120,40,0,0.2); color: #ff8844; } +.zero-engine .condition-tag.sla-panicking { border-color: #882288; background: rgba(120,20,120,0.2); color: #ff88ff; } +.zero-engine .condition-tag.sla-shaken { border-color: #664488; background: rgba(100,40,120,0.18); color: #cc88ff; } +.zero-engine .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; +} +.zero-engine .condition-clear-btn:hover { opacity: 1; } + +/* ── Stress max display ─────────────────────────────────── */ +.zero-engine .stress-max-display { + color: #555; font-size: 14px; margin-left: 2px; align-self: center; +} + +/* ── Drug withdrawal warning ────────────────────────────── */ +.zero-engine .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; +} +.zero-engine .drug-withdrawal-warning i { font-size: 14px; } + +/* ══════════════════════════════════════════════════════════════ + BPN PANEL — original sheet + ══════════════════════════════════════════════════════════════ */ +.zero-engine .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 */ +.zero-engine .bpn-panel.bpn-type-blue { border-color: #1155aa; } +.zero-engine .bpn-panel.bpn-type-red { border-color: #aa1111; } +.zero-engine .bpn-panel.bpn-type-white { border-color: #888; } +.zero-engine .bpn-panel.bpn-type-gold { border-color: #aa8800; } +.zero-engine .bpn-panel.bpn-type-silver { border-color: #778899; } +.zero-engine .bpn-panel.bpn-type-black { border-color: #222; background: rgba(0,0,0,0.5); } +.zero-engine .bpn-panel.bpn-type-grey { border-color: #555; } +.zero-engine .bpn-panel.bpn-type-violet { border-color: #6622aa; } + +.zero-engine .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); +} +.zero-engine .bpn-header-left { display: flex; align-items: center; gap: 10px; } +.zero-engine .bpn-header-right { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; } + +.zero-engine .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; +} +.zero-engine .bpn-type-blue .bpn-type-badge { background: rgba(20,80,180,0.3); color: #88bbff; border: 1px solid #3366cc; } +.zero-engine .bpn-type-red .bpn-type-badge { background: rgba(180,20,20,0.3); color: #ff8888; border: 1px solid #cc3333; } +.zero-engine .bpn-type-white .bpn-type-badge { background: rgba(200,200,200,0.15); color: #ddd; border: 1px solid #888; } +.zero-engine .bpn-type-gold .bpn-type-badge { background: rgba(180,140,0,0.25); color: #ffdd66; border: 1px solid #aa9900; } +.zero-engine .bpn-type-silver .bpn-type-badge { background: rgba(120,140,160,0.2); color: #aabbcc; border: 1px solid #778899; } +.zero-engine .bpn-type-black .bpn-type-badge { background: rgba(0,0,0,0.6); color: #999; border: 1px solid #333; } +.zero-engine .bpn-type-grey .bpn-type-badge { background: rgba(80,80,80,0.3); color: #aaa; border: 1px solid #555; } +.zero-engine .bpn-type-violet .bpn-type-badge { background: rgba(100,30,170,0.3); color: #cc88ff; border: 1px solid #7733bb; } + +.zero-engine .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; +} +.zero-engine .bpn-type-select, .zero-engine .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; +} +.zero-engine .bpn-reward-field { display: flex; align-items: center; gap: 5px; font-size: 12px; color: #aaa; } +.zero-engine .bpn-reward-input { width: 70px; } + +.zero-engine .bpn-body { padding: 8px 12px; display: flex; flex-direction: column; gap: 8px; } +.zero-engine .bpn-field { display: flex; flex-direction: column; gap: 3px; } +.zero-engine .bpn-field-label { font-size: 11px; text-transform: uppercase; letter-spacing: 1px; color: #888; } +.zero-engine .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; +} +.zero-engine .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 ══ */ +.zero-engine .morph-form-panel { + background: rgba(0,0,0,0.2); border: 1px solid #333; border-radius: 5px; + padding: 10px 12px; margin-bottom: 14px; +} +.zero-engine .morph-form-buttons { + display: flex; gap: 8px; margin: 8px 0; +} +.zero-engine .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; +} +.zero-engine .morph-form-btn:hover { border-color: #888; color: #fff; } +.zero-engine .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); +} +.zero-engine .instability-warning { font-size: 11px; color: #ff8800; margin-left: 8px; } +.zero-engine .instability-warning.active { animation: pulse-lethal 1.5s infinite; } +.zero-engine .morph-instability-bar { display: flex; align-items: center; gap: 8px; margin-top: 8px; } +.zero-engine .instability-track { display: flex; gap: 3px; flex: 1; } +.zero-engine .instability-pip { + width: 16px; height: 16px; border-radius: 3px; + border: 1px solid #444; background: rgba(255,255,255,0.05); flex-shrink: 0; +} +.zero-engine .instability-pip.pip-filled { background: #ff8800; border-color: #cc6600; } +.zero-engine .instability-pip.pip-filled.pip-danger { background: #ff2200; border-color: #cc1100; animation: pulse-lethal 1.5s infinite; } +.zero-engine .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; +} +.zero-engine .instability-roll-btn:hover { background: rgba(255,100,0,0.35); } +/* Vevaphon instability vital box */ +.zero-engine .vevaphon-vital { border-color: #ff8800; background: rgba(80,40,0,0.15); } +.zero-engine .vevaphon-vital label { color: #ff8800; } +.zero-engine .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 */ +.zero-engine .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-shrink: 0; +} +.zero-engine .morph-shift-btn:hover { background: rgba(0,180,255,0.25); } +.zero-engine .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); +} diff --git a/system.json b/system.json new file mode 100644 index 0000000..e1df9f8 --- /dev/null +++ b/system.json @@ -0,0 +1,33 @@ +{ + "id": "zero-engine", + "title": "Zero Engine (SLA Industries)", + "description": "A basic game system for SLA Industries using the Zero Engine rules.", + "version": "1.3.0-dev", + "compatibility": { + "minimum": 13, + "verified": "14.359" + }, + "authors": [ + { + "name": "System Developer" + } + ], + "scripts": [], + "esmodules": ["zero-engine.mjs"], + "styles": ["styles/zero-engine.css"], + "languages": [ + { + "lang": "en", + "name": "English", + "path": "lang/en.json" + } + ], + "socket": false, + "initiative": "@combat.initiative", + "grid": { + "distance": 1, + "units": "m" + }, + "primaryTokenAttribute": "health", + "secondaryTokenAttribute": null +} diff --git a/template.json b/template.json new file mode 100644 index 0000000..195cbce --- /dev/null +++ b/template.json @@ -0,0 +1,325 @@ +{ + "Actor": { + "types": [ + "character", + "npc" + ], + "character": { + "biography": "", + "details": { + "age": "", + "height": "", + "weight": "", + "scl": 10, + "credits": 500 + }, + "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 + }, + "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..906a004 --- /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..722e234 --- /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..3308361 --- /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..222a930 --- /dev/null +++ b/templates/actor/character-sheet.hbs @@ -0,0 +1,985 @@ +
    + + {{!-- 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}} + +
    + {{/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}} + {{!-- 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}} +
    + +
    +
    +
    Weekly Income
    +
    c
    +
    c
    +
    c
    +
    {{weeklyIncome}}c
    +
    +
    +
    Weekly Expenses
    +
    c
    +
    c
    +
    c
    +
    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

    + +

    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..ef06686 --- /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/zero-engine/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/zero-engine/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..18a4b84 --- /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..84a07ea --- /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}} +
    diff --git a/zero-engine-state.md b/zero-engine-state.md new file mode 100644 index 0000000..26dd501 --- /dev/null +++ b/zero-engine-state.md @@ -0,0 +1,498 @@ +# Zero Engine (SLA Industries) — System State Reference + +**Last updated:** 2026-06-01 +**System version:** 1.3.0-dev +**Foundry compatibility:** v13 minimum, verified v14.361 +**World:** SLA_Industries (last played 2026-06-01, ~110 hours playtime) +**Single source file:** `zero-engine.mjs` (~6500+ lines) + +--- + +## File Map + +| Path | Role | +|------|------| +| `system.json` | Manifest — id: `zero-engine`, entry: `zero-engine.mjs` | +| `zero-engine.mjs` | Everything: classes, hooks, data tables, PC/NPC creators | +| `styles/zero-engine.css` | All CSS (~4600+ lines, SLA Borg font family) | +| `blade-runner.css` / `.js` | Legacy Blade Runner skin (not SLA-active) | +| `template.json` | Actor & Item data schemas | +| `lang/en.json` | English localisation strings | +| `templates/actor/character-sheet.hbs` | Primary PC sheet (Mk1) | +| `templates/actor/character-sheet-mk2.hbs` | Mk2 dark sheet variant | +| `templates/actor/npc-sheet.hbs` | NPC sheet | +| `templates/actor/vehicle/vehicle-sheet.hbs` | Vehicle sheet | +| `templates/actor/loot/loot-sheet.hbs` | Loot container sheet | +| `templates/actor/character/` | Modular tab partials (stats, combat, inventory, mods, bio) | +| `templates/components/roll/roll-chatcard.hbs` | Roll result chat card | +| `templates/item/item-sheet.hbs` | Item sheet | +| `packs/armors.json` | 6 armors | +| `packs/drugs.json` | 7 drugs | +| `packs/ebb-formulae.json` | 30 Ebb formulae | +| `packs/specialties.json` | 16 specialties | +| `packs/tables-data.json` | 5 rollable tables (panic, phys crit, mental crit, ranged fail, melee fail) | +| `fonts/SLABorg-*.woff2` | Custom SLA Borg font family (Book, Tech, Title, UI) | +| `assets/icons/ebb/` | 10 Ebb discipline SVG icons | +| `assets/racial/` | 8 racial ability SVG icons | +| `assets/textures/` | Chat card, character sheet, journal background webps | + +--- + +## Actor Types + +### `character` (PC) + +**Attributes** (base 2 each, rolled as Nd6): +- Strength, Agility, Wits, Empathy + +**Skills** (add to attribute for dice pool): +| Skill | Attribute | +|-------|-----------| +| Force, Melee, Stamina | Strength | +| Marksmanship, Mobility, Stealth | Agility | +| Crafting, Observation, Survival | Wits | +| Healing, Insight, Persuasion, Ebb | Empathy | + +**Derived Stats:** +- `health` (value/max) — base 3, modified by race/training/armor/drugs/specialties +- `resolve` (value/max) — base 3, modified by race/training/drugs/specialties +- `stress` — scalar 0–10; adds stress dice to all rolls (1s on stress dice = Banes) +- `carryLimit` — base 4 +- `broken` — boolean; triggers Broken condition + +**Other Fields:** +- `details` — age, height, weight, scl (default 10), credits (default 500) +- `race` — string key (see Races below) +- `training` — array, first entry used as archetype label +- `archetype` — string +- `flux` (value/max) — Ebb resource; max = Ebb skill + 1 +- `criticalInjuries` — physical\[\] and mental\[\] arrays +- `panic` — directive string, active boolean +- `vevaphon` — instability (0–12), instabilityMax (12), activeMorphForm +- `finances` — income (salary, bpnReward, other), expenses (accommodation, drugs, subscriptions, other, bulletTax), debt, ammoSpentSession, creditLog\[\] +- `bpn` — code, type (blue/green/red/silver/gold/black), status, reward, description, objectives, notes +- `specialties` / `equipment` / `weapons` — legacy text fields (items are embedded) + +**Health/Resolve calculation** (`calculateHealthResolve`): +``` +healthMax = 2 + (STR-2) + raceMod + trainingMod + equipmentMod + specialtyMod + drugMod +resolveMax = 2 + (EMP-2) + raceMod + trainingMod + equipmentMod + specialtyMod + drugMod +``` +Recalculates on: attribute changes, race change, item add/remove/update, active effect changes. + +--- + +### `npc` (NPC) + +Simplified schema: biography, threat (1–5), flat attributes (str/agi/wits/emp), health (value/max), armor (flat number), damage. + +--- + +## Item Types + +| Type | Key Fields | +|------|-----------| +| `weapon` | weaponType, category (ranged/melee), damage, range, rof, gearBonus, ap, magazine, ammo, fireModes, autoAmmoUse, ammoType, caliber, roundType, initiativeMod, equipped, ammoEmpty | +| `armor` | armorRating, armorDice, armorAuto, statMod, skillMod, statModTarget, skillModTarget, healthMod, resolveMod, initiativeMod, equipped | +| `equipment` | quantity, cost, weight, isDrug, active, activeDuration, healthMod, resolveMod, statPhysicalMod, skillAllMod, panicReduction, injuryPenaltyIgnore, stressRecoveryBonus, postUseStaminaDamage, withdrawal variants of all mods, per-skill mods, initiativeMod, equipped | +| `ammo` | caliber, roundType, quantity, costPerRound, apBonus, damageMod, blastRadius, weight | +| `specialty` | category, effects, prerequisites, package, healthMod, resolveMod, racialBonuses\[\], isActive | +| `ebb` | discipline, fluxCost, successes, effect, catastrophe, duration, range | + +--- + +## Races + +8 playable races, defined in `SLA_RACES`: + +| Race | Key | Base Attrs | Attr Points | Skill Points | Health Mod | Resolve Mod | Notes | +|------|-----|------------|-------------|--------------|-----------|------------|-------| +| Human | `human` | 2/2/2/2 | 6 | 10 | 0 | 0 | Caps 5/5/5/5 | +| Ebon | `ebon` | 2/2/3/3 | 4 | 10 | 0 | +1 | Ebb user. Caps 4/4/5/5 | +| Brain Waster | `brainwaster` | 2/2/4/3 | 3 | 10 | 0 | 0 | Ebb user, push risk catastrophe. Caps 3/4/5/5 | +| Stormer 313-S | `stormer` | 4/3/2/2 | 3 | 8 | +2 | 0 | Caps 5/5/3/3 | +| Shaktar | `shaktar` | 3/3/2/2 | 4 | 9 | +1 | 0 | Caps 5/5/4/4 | +| Wraith Raider | `wraithraider` | 2/4/2/2 | 4 | 10 | 0 | +1 | Caps 4/5/5/4 | +| Frother | `frother` | 4/2/2/2 | 4 | 8 | +1 | -1 | Drug Dependency: -1 all rolls when no drug active. Caps 5/4/4/3 | +| Stormer Vevaphon | `vevaphon` | 3/3/2/2 | 5 | 8 | +2 | -1 | Morph forms + Instability. Caps 5/5/4/3 | + +**Race-derived derived stats** are applied in `RACE_DERIVED_MODS` and `calculateHealthResolve`. Race change auto-adds racial specialty items via `_ensureRacialAbilities`. + +--- + +## Racial Abilities (auto-embedded on race set) + +Each race gets specialty items automatically added to the actor: + +| Race | Specialties Added | +|------|------------------| +| Human | Social Versatility (+1 Persuasion, +1 Insight) | +| Ebon | Ebb Sensitive, natural Ebb formulae | +| Brain Waster | Ebb Fury (push = catastrophe risk), natural Ebb formulae | +| Stormer | Regeneration (regen HP on rest) | +| Shaktar | Warrior Caste (+1 Force, +1 Melee), Code of Honor | +| Wraith Raider | Shaper Senses (+2 Stealth, +1 Observation), Cold Adapted | +| Frother | Drug Dependency, Combat Drug User (+2 Force/Melee while drugged), Blade Proficiency | +| Vevaphon | Brute Form, Stalker Form, Raptor Form, Morphic Strike | + +--- + +## Vevaphon Morph Forms + +Switching form costs 1 Instability and triggers an automatic instability check roll (1d6 + current instability). Forms are stored as specialty items. `activeMorphForm` key: `none` / `brute` / `stalker` / `raptor`. + +| Form | Bonuses | Restriction | +|------|---------|-------------| +| Base (`none`) | Normal stats | — | +| Brute | +2 Health max, knockdown immune, +1 Force, +1 Melee | Cannot be disguised | +| Stalker | +2 Stealth, +1 Agility dice | Cannot wear armour | +| Raptor | +2 Melee, +1 Marksmanship (close range), crits +1 dmg | — | +| Morphic Strike | Natural weapon DMG 2 AP 1 | Always available, all forms | + +**Instability table** (roll: 1d6 + instability value, triggers at result ≥ 7): + +| Range | Name | Severity | +|-------|------|----------| +| 7 | Flicker | minor | +| 8 | Skin Crawl | minor | +| 9 | Drift | minor | +| 10 | Identity Slip | minor | +| 11 | Voice Shift | minor | +| 12 | Pain Spike (+1 Stress) | moderate | +| 13 | Limb Fault (-2 phys) | moderate | +| 14 | Form Bleed | moderate | +| 15 | Sensory Overload | moderate | +| 16 | Tissue Rejection (1 dmg, no armour) | moderate | +| 17 | Threat Lock | serious | +| 18 | Form Panic (+1 Stress) | serious | +| 19 | Memory Fracture | serious | +| 20 | Partial Collapse (1d3 dmg) | serious | +| 21 | Identity Bleed | serious | +| 22 | Uncontrolled Shift (+1 Instability) | serious | +| 23 | Trauma Response (Shaken) | serious | +| 24 | Permanent Scar | critical | +| 25 | Full Reversion (-1 Instability, revert) | critical | +| 26 | Psychic Scream | critical | +| 27 | Body Horror | critical | +| 28 | Cascade Shift (1d3 dmg, +1 Instab) | critical | +| 29 | Dissolution Risk | critical | +| 30 | Identity Death | catastrophic | +| 31 | Organ Shift (2 dmg/round) | catastrophic | +| 32 | Full System Failure | catastrophic | +| 33 | Catastrophic Cascade (1d6 dmg/round) | catastrophic | +| 34 | Biogenetic Detonation (2d6 self, 1d6 AoE) | catastrophic | +| 35 | Terminal Dissolution | lethal | +| 36+ | Total Collapse (instant death) | lethal | + +Auto-effects are applied programmatically (`autoStress`, `autoDamage`, `autoDamageRoll`, `autoInstability`, `revertForm`, `applyCondition`). + +--- + +## Training Packages + +8 packages in `SLA_TRAINING`, selected at PC creation: + +| Package | Key | Starting Skills | Specialty | Armour | +|---------|-----|-----------------|-----------|--------| +| Soldier | `soldier` | Melee 2, Marksmanship 2, Stamina 1 | Hard Hitter | CAF Armor | +| Scout | `scout` | Stealth 2, Mobility 2, Observation 1 | Fast Reflexes | Scout Armor | +| Combat Medic | `medic` | Healing 2, Insight 1, Observation 2 | Field Surgeon | Downtown Jacket | +| Technical Operative | `tech` | Crafting 3, Observation 1, Survival 1 | Inquisitive | Downtown Jacket | +| Negotiator | `negotiator` | Persuasion 2, Insight 2, Observation 1 | Gut Feeling | Downtown Jacket | +| Hunter | `hunter` | Marksmanship 2, Stealth 2, Observation 1 | Sniper | Scout Armor | +| Brawler | `brawler` | Force 2, Melee 2, Stamina 1 | True Grit | CAF Armor | +| Ebb Channeller | `ebb_user` | Ebb 3, Insight 1, Persuasion 1 | Ebb Sensitive | Downtown Jacket | + +Ebb Channeller gives race-differentiated starting formulae: +- **Ebon:** Empathic Pulse, Danger Sense, Mind-Link, Calm, Ebb Shield, Ebb Sight, Surge +- **Brain Waster:** Ebb Bolt, Heat Touch, Surge, Push, Empathic Pulse, Ebb Shield, Iron Flesh + +--- + +## NPC Archetypes + +10 archetypes in `SLA_NPC_TYPES` (used by `SLANPCCreator`): + +| Archetype | Threat | HP | Armor | +|-----------|--------|----|-------| +| Civilian | 1 | 4 | 0 | +| Gang Grunt | 1 | 5 | 1 | +| Shiver (Patrol) | 2 | 6 | 2 | +| SLA Operative (Green) | 2 | 5 | 2 | +| Carrien | 2 | 7 | 1 | +| SLA Operative (Veteran) | 3 | 8 | 3 | +| Ebb Operative (Ebon) | 3 | 6 | 1 | +| Dept. of Propaganda Agent | 3 | 6 | 2 | +| Dark Finder | 4 | 10 | 4 | +| *(10th not captured in this read)* | — | — | — | + +--- + +## Core Roll Mechanics + +### Dice Pool + +- Roll **Attribute + Skill** d6s (plus Gear Bonus for weapons) +- **Success** = 6 on any die +- Count total successes (each 6 = 1 success) +- 0 successes = failure + +### Stress Dice + +- `stress` value adds that many d6s to every roll +- 1s on stress dice = **Banes** (tracked, can trigger panic) +- Stress increases on push; some drugs/drugs-withdrawal affect stress + +### Push + +- After a failed roll, player may push (re-roll non-6, non-1 dice) +- **Cost:** +1 Stress +- **Brain Wasters:** pushing risks catastrophe result +- Results formatted with `_formatYZEResult` and displayed in chat card + +### Armor Check + +- Triggered when character takes damage +- Roll **armorDice** d6s, subtract **AP** from dice pool first +- Successes reduce damage 1:1 +- `armorAuto` = automatic damage reduction (no roll needed) +- Called via `_promptAndRollArmorCheck` → `_rollArmorCheck` + +### Weapon Rolls + +- **Ranged:** AGI + Marksmanship + Gear Bonus +- **Melee:** STR + Melee + Gear Bonus +- Fire modes: single, burst, full-auto (different ammo costs) +- `autoAmmoUse`: ammo consumed on full-auto (default 8) +- Bullet Tax auto-applied after session (tracked in `finances.bulletTax`) + +### Ebb Rolls + +- Roll **EMP + Ebb Skill** d6s +- Need `successes` threshold from formula item +- Flux cost deducted from `flux.value` +- Catastrophe text displayed on roll if applicable +- Disciplines: awareness, blast, communicate, enhance, heal, protect, realityFold, senses, telekinesis, thermal + +--- + +## Conditions (SLA_CONDITIONS) + +14 registered status effects, auto-applied via `applyCondition` or Token HUD: + +| ID | Label | Penalty | +|----|-------|---------| +| `sla-bleeding` | Bleeding | -1 HP/turn | +| `sla-stunned` | Stunned | -3 all dice, clears end of turn | +| `sla-pinned` | Pinned | -2 melee/marksmanship/mobility | +| `sla-suppressed` | Suppressed | -2 all dice | +| `sla-on-fire` | On Fire | -2+ HP/turn (escalating), -1 all dice | +| `sla-blinded` | Blinded | -3 ranged/observation, -2 melee | +| `sla-deafened` | Deafened | -1 observation | +| `sla-broken` | Broken | -2 all dice, must roll Stamina to act | +| `sla-winded` | Winded | -1 all dice, clears end of round | +| `sla-broken-arm` | Broken Arm | -2 Melee, -1 Marksmanship, -1 Force | +| `sla-broken-leg` | Broken Leg | -2 Mobility, -1 Stealth | +| `sla-concussed` | Concussed | -1 all, extra -1 Observation/Wits | +| `sla-gut-wound` | Gut Wound | -2 all dice | +| `sla-panicking` | Panicking | -2 all dice, cannot act normally | +| `sla-shaken` | Shaken | -1 all dice | + +Condition dice penalties are applied via `_getConditionModifiers(actor, {skill, attribute})` and reflected in every roll. + +--- + +## Tables (tables-data.json) + +| Table | Formula | +|-------|---------| +| Panic | 1d6 + Stress | +| Physical Critical | 1d66 | +| Mental Critical | 1d66 | +| Ranged Critical Fail | 1d6 | +| Melee Critical Fail | 1d6 | + +--- + +## Equipment Packs + +### Armors (6) + +| Name | AR | Dice | Auto | +|------|----|------|------| +| Downtown Jacket | 1 | 1 | 0 | +| CAF Armor | 2 | 2 | 1 | +| Scout Armor | 3 | 3 | 1 | +| HARD Armor | 4 | 4 | 2 | +| Exo-Rig Armor | 5 | 5 | 2 | +| Squad Shielded Power Armor | 6 | 6 | 3 | + +### Drugs (7) + +| Name | HP | Resolve | Str | Panic Red | +|------|----|---------|----|-----------| +| Painkillers | 0 | +1 | 0 | 0 | +| Stimulant | 0 | +1 | +1 | 0 | +| Sedative | 0 | +2 | 0 | 0 | +| Redline | +1 | -2 | +2 | 0 | +| Calm-Line | 0 | +2 | 0 | +2 | +| Rush | +1 | -1 | +1 | 0 | +| UV (Ultra-Violence) | +2 | -3 | +2 | 0 | + +All drugs track active/withdrawal states with separate modifier sets. + +### Specialties (16 in system pack) + +Categories: combat, investigation, social, medical, ebb, racial, survival + +| Name | Category | Package | +|------|----------|---------| +| Sniper | combat | Scout | +| Fast Reflexes | combat | Scout | +| Inquisitive | investigation | Investigator | +| Gut Feeling | investigation | Investigator | +| Hard Hitter | combat | Frother | +| True Grit | survival | Frother | +| Menacing | social | Corporate | +| Compassion | social | Corporate | +| Field Surgeon | medical | Medic | +| Healer | medical | Medic | +| Ebb Sensitive | ebb | Ebon - Any | +| Ebb Fury | ebb | Brain Waster - Operative | +| Code of Honor | racial | Shaktar | +| Cold Adapted | racial | Wraith Raider | +| Regeneration | racial | Stormer | +| Combat Drug User | racial | Frother | + +### Ebb Formulae (30) + +| Discipline | Formulae | +|-----------|---------| +| Awareness | Empathic Pulse (1), Precognition (2), Danger Sense (2) | +| Blast | Ebb Bolt (1), Hellfire (2), Ebb Storm (3) | +| Communicate | Mind-Link (1), Broadcast (2), Compel (2) | +| Enhance | Surge (1), Iron Flesh (2), Heightened State (3) | +| Heal | Mend (1), Restore (2), Calm (1) | +| Protect | Ebb Shield (1), Ward (2), Phase Shell (3) | +| RealityFold | Blink (2), Fold Space (3), Phase Through (3) | +| Senses | Ebb Sight (1), Mind's Eye (1), Remote View (2) | +| Telekinesis | Push (1), Grip (2), Levitate (2) | +| Thermal | Heat Touch (1), Flame Burst (2), Freeze (2) | + +*(Numbers in parentheses = Flux cost)* + +--- + +## Sheets + +### Registered Sheets + +| Class | Type | Label | Default | +|-------|------|-------|---------| +| `ZeroEngineActorSheet` | character + npc | Zero Engine Character Sheet | Yes | +| `ZeroEngineActorSheetMk2` | character only | Zero Engine — Mk2 (Dark) | No | +| `ZeroEngineItemSheet` | all items | Zero Engine Item Sheet | Yes | + +### Mk1 Sheet Tabs (character-sheet.hbs) + +Stats, Combat, Inventory, Mods, Bio — via `templates/actor/character/sheet-tabs/` + +### Mk2 Sheet (ZeroEngineActorSheetMk2) + +Dark-themed variant extending `ZeroEngineActorSheet`. Uses `character-sheet-mk2.hbs`. Full SLA Borg font/dark palette. + +--- + +## PC Creator Wizard (`SLAPCCreator`) + +Multi-step Application launched from Actor Directory. Steps: +1. Choose race (8 options with flavor text) +2. Allocate attribute points (race-specific pool and caps) +3. Choose training package (some race-restricted, e.g. Ebb Channeller = Ebon/Brain Waster only) +4. Allocate skill points (race-specific pool, cap 3) +5. Random name generation (race-keyed name tables) +6. Confirm & create actor with all embedded items + +Racial abilities auto-embedded via `_ensureRacialAbilities` on actor creation when race is set. + +--- + +## NPC Creator (`SLANPCCreator`) + +Two modes: **Quick** (pick archetype, auto-fill stats) and **Full** (manual stat entry). Generates NPC actor with appropriate weapons, armor, and attributes. + +--- + +## Hooks Registered + +| Hook | Purpose | +|------|---------| +| `init` | Register sheets, Handlebars helpers, SLA conditions setup trigger | +| `setup` | Register `SLA_CONDITIONS` as Foundry status effects | +| `ready` (×3) | DiceSoNice skin, PC creator button in Actor Directory, world clock | +| `createCombatant` | Auto-initiative handling | +| `renderCombatTracker` | Custom combat UI additions | +| `diceSoNiceReady` | Register custom SLA Borg d6 dice skin | +| `renderChatMessageHTML` | Post-roll push buttons, apply damage buttons | +| `preUpdateActor` | Guard recalc triggers | +| `createActor` | Auto-derive stats + racial abilities | +| `updateActor` (×2) | Recalc derived stats; handle race change → racial abilities | +| `createItem` / `updateItem` / `deleteItem` | Recalc derived stats (armor/drug/specialty mods) | +| `createActiveEffect` / `updateActiveEffect` / `deleteActiveEffect` | Recalc derived stats | +| `renderActorDirectory` | Inject "New SLA Operative" PC creator button | +| `updateWorldTime` | Shift Ledger world clock integration | +| `combatTurnChange` | Per-turn condition ticks (bleeding, on fire, etc.) | +| `renderTokenHUD` | Condition toggle buttons in token HUD | +| `pauseGame` | Custom SLA pause animation | + +--- + +## Modules + +All modules are standalone FormApplication/Application classes targeting the `zero-engine` system. + +### sla-bpn-dispatch (v0.3.0) + +Generates random BPN briefings using SLA mission colour logic (blue/green/red/silver/gold/black). Builds contact name, objectives, scenario frame, reward package. Creates GM debrief journal entry. Integrated into Actor Directory. + +### sla-briefing-desk (v0.1.0) + +Mission briefing composer. Draft saved to world settings. Renders HTML briefing and creates journal entry. Adds button to Journal Directory. + +### sla-gear-cache (v0.1.0) + +Generates ammo caches, med packs, breach kits, requisition bundles. Adds generated items directly to selected actor. Adds button to Actor Directory. + +### sla-npc-director (v0.1.0) + +Tracks NPC morale, suppression, stance, and intent. Reads from current combatants or selected tokens. Saves state to world settings. Accessible from Combat Tracker, Actor Directory, and Settings. + +### sla-ops-clock (v0.1.0) + +Tracks mission tempo: operation clock, heat, collateral, pressure. Posts player-facing updates to chat. State saved to world settings. Adds button to Scene Directory. + +### sla-shift-ledger (v0.2.0) + +Full campaign time tracker. Manages: day/week/shift progression, actor duty schedules, housing tiers (with weekly cost calculation), living cost ledger. `SlaCampaignClockApp` application. Integrates with `updateWorldTime` hook. + +--- + +## World Compendium Packs (SLA_Industries world) + +| Pack Name | Type | Contents | +|-----------|------|----------| +| `specialities` | Item | World-level specialty items (LevelDB) | +| `sla-specialties` | Item | SLA specialty items (LevelDB) | +| `sla-weapons` | Item | SLA weapon items (LevelDB) | + +--- + +## Known Issues / Notes for Development + +- `template.json` still lists `vehicle` as an actor type in world notes but is not in the schema `types` array — vehicle sheet exists as a template but is not formally registered as an actor type. +- Ebb `flux.max` is computed dynamically (Ebb skill + 1) rather than stored; `flux.value` is stored. +- `race` field is typed as `Array` in `template.json` but consumed as a string throughout the code (`race[0]` with fallback). This is a legacy inconsistency. +- Bullet Tax tracks `ammoSpentSession` on the actor and applies costs at rest. +- `blade-runner.css` / `blade-runner.js` are present but not loaded — legacy files from the base YZE system. +- `lang/` contains de, en, es, fr, ko, pl — only en is actively maintained for SLA content. +- DiceSoNice skin registered as `sla-borg-d6` using custom SFX config at `assets/plugins/dice-so-nice/`. diff --git a/zero-engine.mjs b/zero-engine.mjs new file mode 100644 index 0000000..eebba7d --- /dev/null +++ b/zero-engine.mjs @@ -0,0 +1,6536 @@ +// Zero Engine System +console.log('Zero Engine | 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 Zero Engine 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) => { + 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; + // Multi-shot options (passed from _onWeaponRoll) + 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; + // Multi-shot bonus from shot selector + 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; + const totalDice = Math.max(0, baseDice + modifier + modeBonus + ammoBonus + rangeBonus + scopeBonus + aiBonus + shotDiceBonus); + if (options.returnDetails) { + resolve({ totalDice, fireMode, ammoType, modifier, rangeBand, useScope, useAI, rangeBonus, scopeBonus, aiBonus, shotsChosen, shotDiceBonus }); + } 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'); + + // Update total when input changes + 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(0, baseDice + modifier + modeBonus + ammoBonus + rangeBonus + scopeBonus + aiBonus); + totalDisplay.text(`${total}d6`); + }; + + 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); + + // Handle modifier buttons + 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(); + }); + } + }, { + width: 400, + classes: ["zero-engine", "dialog"] + }); + + dialog.render(true); + }); + } +} + +/** + * Zero Engine Item Sheet + */ +class ZeroEngineItemSheet extends foundry.appv1.sheets.ItemSheet { + /** @override */ + static get defaultOptions() { + return foundry.utils.mergeObject(super.defaultOptions, { + classes: ["zero-engine", "sheet", "item"], + width: 520, + height: 480, + tabs: [] + }); + } + + /** @override */ + get template() { + return `systems/zero-engine/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; + } +} + +/** + * Zero Engine Actor Sheet + */ +class ZeroEngineActorSheet extends foundry.appv1.sheets.ActorSheet { + /** @override */ + static get defaultOptions() { + return foundry.utils.mergeObject(super.defaultOptions, { + classes: ["zero-engine", "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/zero-engine/templates/actor/character-sheet.hbs`; + } + return `systems/zero-engine/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) + 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); + 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 raceRaw = this.actor.system.race; + const race = String(Array.isArray(raceRaw) ? (raceRaw[0] || "") : (raceRaw || "")).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 + html.find('.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 buttons + html.find('.skill-roll').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)); + + // 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 interactions - click name to open sheet + html.find('.weapon-item .weapon-name').click(ev => { + const itemId = ev.currentTarget.closest('.weapon-item').dataset.itemId; + const item = this.actor.items.get(itemId); + if (item) item.sheet.render(true); + }); + + // 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 }); + ui.notifications.info(`Condition cleared: ${condId.replace('sla-','')} on ${this.actor.name}`); + } catch(e) { + console.warn('Zero Engine | Could not clear condition:', e); + } + }); + + // 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 parts = []; + if (health !== 0) parts.push(`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 + */ + 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; + } + + await item.update({ + "system.quantity": qty - 1, + "system.active": true, + "system.withdrawalActive": false + }); + + 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)); + } + } + + 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); + + await item.update({ + "system.active": false, + "system.withdrawalActive": hasWithdrawal + }); + + 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; + await item.update({ "system.withdrawalActive": false }); + 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 attributeValue = parseInt(this.actor.system.attributes[attribute]?.value) || 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 totalDice = (empathy + ebbSkill) || 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} = ${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") { + const hpMax = actor.system.health?.max ?? actor.system.attributes?.health?.max ?? 10; + const hpCurrent = actor.system.health?.value ?? actor.system.attributes?.health?.value ?? hpMax; + 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.health.value": newHp }); + ui.notifications.info(`${actor.name} | Mend: +${newHp - hpCurrent} HP restored.`); + } + } 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.health.value": newHp }); + ui.notifications.info(`${actor.name} | Restore: +${newHp - hpCurrent} HP restored.`); + } + } 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 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 totalDice = Math.max(0, attributeValue + armorMods.total + drugMods.total); + 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}` : ''}` + + ` = ${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 totalDice = Math.max(0, attributeValue + skillValue + armorMods.total + drugMods.total); + 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}` : ''}` + + ` = ${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; + const itemId = button.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"; + + const attrValue = parseInt(this.actor.system.attributes[attribute]?.value) || 2; + 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 // ← multi-shot selector + } + ); + + 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} round${ammoSpent !== 1 ? 's' : ''} × ${cpr}c = −${taxPaid}c` + ); + } + } + + // 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)), + 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 + for (const term of (roll.dice || [])) { + term.options = term.options || {}; + term.options.colorset = 'ze-normal'; + term.options.system = 'zero-engine'; + } + + 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) { + try { + await actor.toggleStatusEffect(entry.applyCondition, { active: true }); + notes.push(`Condition applied: ${entry.applyCondition.replace('sla-','')}`); + } catch(_) {} + } + + 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 Zero Engine 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); + 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: { + 'zero-engine': { + 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 cpr = weaponInfo.bulletTaxPaid > 0 + ? ` · 💸 Tax: −${weaponInfo.bulletTaxPaid}c` + : ''; + const calLabel = weaponInfo.caliber ? ` [${weaponInfo.caliber} ${weaponInfo.roundType || 'std'}]` : ''; + html += `
    Ammo Spent: ${weaponInfo.ammoSpent}${calLabel} (Remaining ${weaponInfo.ammo}/${weaponInfo.magazine})${cpr}
    `; + } + 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['zero-engine']; + 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: { + 'zero-engine': { + 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 — DsN picks this up automatically from rolls:[roll] in ChatMessage + for (const term of (roll.dice || [])) { + term.options = term.options || {}; + term.options.colorset = "ze-normal"; + term.options.system = "zero-engine"; + } + + 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 + try { + const panicLevel = total || 0; + if (panicLevel >= 10) { + await actor.toggleStatusEffect('sla-panicking', { active: true }); + ui.notifications.warn(`${actor.name} is PANICKING! (-2 dice to all rolls)`); + } else if (panicLevel >= 7) { + await actor.toggleStatusEffect('sla-shaken', { active: true }); + ui.notifications.warn(`${actor.name} is SHAKEN! (-1 die to all rolls)`); + } + } catch(_) {} + + 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; + + // ── Roll the same pool for the bonus successes (no stress dice) ────────── + const pool = Math.max(1, baseScore); + 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('zero-engine', '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})
    ` : ''} +
    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.colorset = "ze-normal"; + diceTerms[normalIndex].options.system = "zero-engine"; + } + + // 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.colorset = "ze-stress"; + diceTerms[stressIndex].options.system = "zero-engine"; + } + } + + /** + * 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 — DsN picks this up automatically from rolls:[d66Roll] in ChatMessage + for (const term of (d66Roll.dice || [])) { + term.options = term.options || {}; + term.options.colorset = "ze-normal"; + term.options.system = "zero-engine"; + } + + 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) { + try { + const already = actor.statuses?.has(condId); + if (!already) await actor.toggleStatusEffect(condId, { active: true }); + } catch(_) {} + } + + // 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 — DsN picks this up automatically from rolls:[d66Roll] in ChatMessage + for (const term of (d66Roll.dice || [])) { + term.options = term.options || {}; + term.options.colorset = "ze-normal"; + term.options.system = "zero-engine"; + } + + 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) { + try { + const already = actor.statuses?.has(condId); + if (!already) await actor.toggleStatusEffect(condId, { active: true }); + } catch(_) {} + } + + // 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('Zero Engine | 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); + return vals.some(Boolean) ? opts.fn(this) : opts.inverse(this); + }); + Handlebars.registerHelper('gte', function(a, b) { return Number(a) >= Number(b); }); + Handlebars.registerHelper('not', function(val) { return !val; }); + + console.log('Zero Engine | Handlebars helpers registered'); + + // Register sheet application classes + foundry.documents.collections.Actors.unregisterSheet("core", foundry.appv1.sheets.ActorSheet); + foundry.documents.collections.Actors.registerSheet("zero-engine", ZeroEngineActorSheet, { + makeDefault: true, + label: "Zero Engine Character Sheet" + }); + + // Register item sheet + foundry.documents.collections.Items.unregisterSheet("core", foundry.appv1.sheets.ItemSheet); + foundry.documents.collections.Items.registerSheet("zero-engine", ZeroEngineItemSheet, { + makeDefault: true, + label: "Zero Engine 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("zero-engine", ZeroEngineActorSheetMk2, { + types: ["character"], + makeDefault: false, + label: "Zero Engine — Mk2 (Dark)" + }); + + console.log('Zero Engine | 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("Zero Engine | SLA combat conditions registered"); + }); +}); + +Hooks.once("setup", () => { + // Apply after all module init hooks so card-based module overrides are reverted. + restoreZeroEngineCoreCombatConfig(); + ensureCombatantCompatibilityMethods(); +}); + +/** + * Ready hook + */ +Hooks.once('ready', function() { + console.log('Zero Engine | Ready'); + + // ── 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(`Zero Engine | 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(`Zero Engine | Sanitizing corrupt flux on actor "${actor.name}": "${rawFlux}" → ${updates['system.flux.value']}`); + } + 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", "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/zero-engine/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("Zero Engine | 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/zero-engine/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("Zero Engine | 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/zero-engine/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("Zero Engine | Failed to ensure default ebb formula items", err); + } + + // Ensure critical failure roll tables exist + try { + const response = await fetch("systems/zero-engine/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("Zero Engine | 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); + }); + }); +}); + +// Dice So Nice: Stress dice color set +// ══ Zero Engine — Dice So Nice: Alien RPG dice skin ══════════════════════════ +// Normal pool dice use the Alien yellow d6 (blank/skull/success faces). +// Stress pool dice use the Alien black d6 (blank/success faces). +// Both reference the Alien RPG system's DsN images — that system must be +// Dice images are bundled in systems/zero-engine/assets/dice/alien/ — no dependency on alienrpg. +Hooks.once("diceSoNiceReady", (dice3d) => { + // ── Colorsets ─────────────────────────────────────────────────────────────── + dice3d.addColorset({ + name: "ze-normal", + description: "ZE Normal Dice — Yellow (Alien)", + category: "Zero Engine", + foreground: ["#e3e300"], + background: ["#e3e300"], + outline: "#000000", + texture: "none", + material: "plastic", + visibility: "visible" + }); + + dice3d.addColorset({ + name: "ze-stress", + description: "ZE Stress Dice — Black (Alien)", + category: "Zero Engine", + foreground: ["#ffffff"], + background: ["#111111"], + outline: "#333333", + texture: "none", + material: "plastic", + visibility: "visible" + }); + + // ── Register the Zero Engine dice system ──────────────────────────────────── + dice3d.addSystem({ id: "zero-engine", name: "Zero Engine (SLA Industries)" }, "preferred"); + + // ── Normal d6: Alien yellow dice ──────────────────────────────────────────── + // Face 1 = skull (bane), 2-5 = blank, 6 = alien success symbol + dice3d.addDicePreset({ + type: "d6", + labels: [ + "systems/zero-engine/assets/dice/alien/alien-dice-y1.png", + "systems/zero-engine/assets/dice/alien/alien-dice-y0.png", + "systems/zero-engine/assets/dice/alien/alien-dice-y0.png", + "systems/zero-engine/assets/dice/alien/alien-dice-y0.png", + "systems/zero-engine/assets/dice/alien/alien-dice-y0.png", + "systems/zero-engine/assets/dice/alien/alien-dice-y6.png", + ], + colorset: "ze-normal", + system: "zero-engine", + }); + + // ── Stress d6: Alien black dice ───────────────────────────────────────────── + // Face 1-5 = blank, 6 = success symbol (always triggers panic check on push) + dice3d.addDicePreset({ + type: "d6", + labels: [ + "systems/zero-engine/assets/dice/alien/alien-dice-b0.png", + "systems/zero-engine/assets/dice/alien/alien-dice-b0.png", + "systems/zero-engine/assets/dice/alien/alien-dice-b0.png", + "systems/zero-engine/assets/dice/alien/alien-dice-b0.png", + "systems/zero-engine/assets/dice/alien/alien-dice-b0.png", + "systems/zero-engine/assets/dice/alien/alien-dice-b6.png", + ], + colorset: "ze-stress", + system: "zero-engine", + }); +}); + +/** + * 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?.['zero-engine']; + 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(`Zero Engine | 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("zero-engine", `${stat}Mod`), 0); + const drug = toNumber(effect.getFlag("zero-engine", `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 zero-engine.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/zero-engine/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/zero-engine/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/zero-engine/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/zero-engine/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/zero-engine/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/zero-engine/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/zero-engine/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/zero-engine/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/zero-engine/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/zero-engine/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/zero-engine/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/zero-engine/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/zero-engine/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/zero-engine/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/zero-engine/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/zero-engine/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/zero-engine/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/zero-engine/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/zero-engine/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/zero-engine/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/zero-engine/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/zero-engine/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/zero-engine/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/zero-engine/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/zero-engine/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/zero-engine/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/zero-engine/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/zero-engine/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/zero-engine/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/zero-engine/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/zero-engine/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/zero-engine/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: ['zero-engine', '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('zero-engine.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('Zero Engine | 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: ['zero-engine', '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 +
    +
    + + +
    + + + +
    + +
    + + +
    +
    `; + } +} + +// ── 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)); + + 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/zero-engine/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/zero-engine/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/zero-engine/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/zero-engine/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/zero-engine/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 + } + } + ], + + frother: [ + { + name: "Blade Proficiency", + type: "specialty", + img: "systems/zero-engine/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/zero-engine/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/zero-engine/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/zero-engine/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/zero-engine/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/zero-engine/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/zero-engine/assets/racial/natural-weapons.svg", + system: { + category: "racial", description: "Natural biogenetic weapon formed from the Vevaphon's own mass. Damage 2, AP 1. Always available regardless of morph form. Counts as melee weapon for skill rolls.", + effects: "Natural weapon: DMG 2, AP 1. Always available.", + racialBonuses: [], + prerequisites: "Vevaphon", isActive: true, healthMod: 0, resolveMod: 0 + } + } + ] +}; + +// ─── Auto-embed racial abilities ───────────────────────────────────────────── + +async function _ensureRacialAbilities(actor) { + if (!actor || actor.type !== "character") return; + const raceRaw = actor.system?.race; + const race = String(Array.isArray(raceRaw) ? (raceRaw[0] || "") : (raceRaw || "")) + .toLowerCase() + .replace(/[\s-]/g, "") + .replace("313-s", "") + .trim(); + + const abilities = SLA_RACIAL_ABILITY_DATA[race]; + if (!abilities || !abilities.length) return; + + const toCreate = []; + const toUpdateImg = []; + for (const ability of abilities) { + const existing = actor.items.find( + i => i.type === "specialty" && i.name === ability.name + ); + if (!existing) { + toCreate.push(ability); + } else if (ability.img && existing.img !== ability.img) { + // Update stale icon on existing item + toUpdateImg.push({ _id: existing.id, img: ability.img }); + } + } + if (toUpdateImg.length) { + try { await actor.updateEmbeddedDocuments("Item", toUpdateImg); } 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("Zero Engine | 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 raceRaw = actor.system?.race; + const race = String(Array.isArray(raceRaw) ? (raceRaw[0] || "") : (raceRaw || "")).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(`Zero Engine | 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." } + } + }, + // ── 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." } + } + }, + // ── Assault 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." } + } + }, + // ── Battle rifle / LMG ─────────────────────────────────────────────────── + "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." } + } + }, + // ── 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." } + } + } +}; + +// Weapon type → default caliber mapping (matches weaponType field) +const SLA_WEAPON_DEFAULT_CALIBER = { + "Pistol": "10mm", + "SMG": "9mm", + "Rifle": "5.56mm", + "Assault Rifle": "5.56mm", + "Battle Rifle": "7.62mm", + "Sniper": "7.62mm", + "LMG": "7.62mm", + "HW": "12.7mm", + "Heavy Weapon": "12.7mm", + "Autocannon": "12.7mm" +}; + +/** + * 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. + * Called after a weapon fires. + */ +async function _applyBulletTax(actor, weaponData, roundsFired) { + if (!actor || roundsFired <= 0) return 0; + const costPerRound = _getAmmoCostForWeapon(weaponData); + if (costPerRound <= 0) return 0; + + const totalCost = costPerRound * roundsFired; + const currentCredits = Number(actor.system?.details?.credits ?? 0); + const newCredits = currentCredits - totalCost; + + // Deduct from credits; track session bullet tax + const sessionSpent = Number(actor.system?.finances?.ammoSpentSession ?? 0) + totalCost; + const bulletTaxTotal = Number(actor.system?.finances?.expenses?.bulletTax ?? 0) + totalCost; + + await actor.update({ + "system.details.credits": newCredits, + "system.finances.ammoSpentSession": sessionSpent, + "system.finances.expenses.bulletTax": bulletTaxTotal + }); + + 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/zero-engine/templates/actor/character-sheet-mk2.hbs"; + } + + static get defaultOptions() { + return foundry.utils.mergeObject(super.defaultOptions, { + classes: ["zero-engine", "sheet", "actor"], + template: "systems/zero-engine/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); + 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 ──────────────────────────────────────────────── + +/** + * 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 = []; + + for (const cond of SLA_CONDITIONS) { + if (!statuses.has(cond.id)) continue; + const mods = cond.diceModifiers || {}; + // "all" modifier applies to everything + if (mods.all) { + bonus += mods.all; + breakdown.push(`${cond.label} ${mods.all}`); + continue; + } + // skill-specific modifier + if (skill && mods[skill] !== undefined) { + bonus += mods[skill]; + breakdown.push(`${cond.label} ${mods[skill]}`); + } + } + + // 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 EACH COMBAT TURN ──────────────────────────── + +Hooks.on("combatTurnChange", async (combat, _prior, current) => { + if (!game.user?.isGM) return; + const combatant = combat.combatants.get(current.combatantId); + if (!combatant?.actor) return; + const actor = combatant.actor; + const statuses = actor.statuses ?? new Set(); + + const updates = {}; + const messages = []; + + // Bleeding: -1 HP + if (statuses.has("sla-bleeding")) { + const hp = Number(actor.system?.derivedStats?.health?.value ?? 0); + const newHp = Math.max(0, hp - 1); + updates["system.derivedStats.health.value"] = newHp; + messages.push(`🩸 ${actor.name} bleeds — ${hp} → ${newHp} HP`); + if (newHp <= 0) messages.push(`⚠ ${actor.name} is at 0 HP!`); + } + + // On Fire: -2 HP (escalating tracked via a flag) + if (statuses.has("sla-on-fire")) { + const fireLvl = Number(actor.getFlag("zero-engine", "fireLevel") ?? 1); + const hp = Number((updates["system.derivedStats.health.value"] ?? actor.system?.derivedStats?.health?.value) ?? 0); + const dmg = fireLvl + 1; + const newHp = Math.max(0, hp - dmg); + updates["system.derivedStats.health.value"] = newHp; + await actor.setFlag("zero-engine", "fireLevel", Math.min(fireLvl + 1, 5)); + messages.push(`🔥 ${actor.name} burns (level ${fireLvl}) — ${hp} → ${newHp} HP`); + } + + if (Object.keys(updates).length > 0) { + await actor.update(updates); + } + + if (messages.length > 0) { + ChatMessage.create({ + speaker: { alias: "Combat Conditions" }, + content: `
    ${messages.join('
    ')}
    ` + }); + } +}); + +// 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("zero-engine", "fireLevel"); + if (lvl) await actor.unsetFlag("zero-engine", "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(); + } +});